@teleporthq/teleport-plugin-next-data-source 0.40.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARRAY_MAPPER_PAGINATION.md +1128 -0
- package/LICENSE +21 -0
- package/README.md +40 -0
- package/SEARCH_IMPLEMENTATION_SUMMARY.md +983 -0
- package/__tests__/fetchers.test.ts +545 -0
- package/__tests__/integration.test.ts +561 -0
- package/__tests__/mocks.ts +241 -0
- package/__tests__/pagination.test.ts +31 -0
- package/__tests__/plugin.test.ts +577 -0
- package/__tests__/utils.test.ts +430 -0
- package/__tests__/validation.test.ts +348 -0
- package/dist/cjs/array-mapper-pagination.d.ts +32 -0
- package/dist/cjs/array-mapper-pagination.d.ts.map +1 -0
- package/dist/cjs/array-mapper-pagination.js +77 -0
- package/dist/cjs/array-mapper-pagination.js.map +1 -0
- package/dist/cjs/count-fetchers.d.ts +12 -0
- package/dist/cjs/count-fetchers.d.ts.map +1 -0
- package/dist/cjs/count-fetchers.js +46 -0
- package/dist/cjs/count-fetchers.js.map +1 -0
- package/dist/cjs/data-source-fetchers.d.ts +14 -0
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -0
- package/dist/cjs/data-source-fetchers.js +185 -0
- package/dist/cjs/data-source-fetchers.js.map +1 -0
- package/dist/cjs/fetchers/airtable.d.ts +6 -0
- package/dist/cjs/fetchers/airtable.d.ts.map +1 -0
- package/dist/cjs/fetchers/airtable.js +27 -0
- package/dist/cjs/fetchers/airtable.js.map +1 -0
- package/dist/cjs/fetchers/clickhouse.d.ts +6 -0
- package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -0
- package/dist/cjs/fetchers/clickhouse.js +29 -0
- package/dist/cjs/fetchers/clickhouse.js.map +1 -0
- package/dist/cjs/fetchers/csv-file.d.ts +7 -0
- package/dist/cjs/fetchers/csv-file.d.ts.map +1 -0
- package/dist/cjs/fetchers/csv-file.js +36 -0
- package/dist/cjs/fetchers/csv-file.js.map +1 -0
- package/dist/cjs/fetchers/firestore.d.ts +6 -0
- package/dist/cjs/fetchers/firestore.d.ts.map +1 -0
- package/dist/cjs/fetchers/firestore.js +35 -0
- package/dist/cjs/fetchers/firestore.js.map +1 -0
- package/dist/cjs/fetchers/google-sheets.d.ts +6 -0
- package/dist/cjs/fetchers/google-sheets.d.ts.map +1 -0
- package/dist/cjs/fetchers/google-sheets.js +30 -0
- package/dist/cjs/fetchers/google-sheets.js.map +1 -0
- package/dist/cjs/fetchers/index.d.ts +17 -0
- package/dist/cjs/fetchers/index.d.ts.map +1 -0
- package/dist/cjs/fetchers/index.js +56 -0
- package/dist/cjs/fetchers/index.js.map +1 -0
- package/dist/cjs/fetchers/javascript.d.ts +7 -0
- package/dist/cjs/fetchers/javascript.d.ts.map +1 -0
- package/dist/cjs/fetchers/javascript.js +40 -0
- package/dist/cjs/fetchers/javascript.js.map +1 -0
- package/dist/cjs/fetchers/mariadb.d.ts +3 -0
- package/dist/cjs/fetchers/mariadb.d.ts.map +1 -0
- package/dist/cjs/fetchers/mariadb.js +23 -0
- package/dist/cjs/fetchers/mariadb.js.map +1 -0
- package/dist/cjs/fetchers/mongodb.d.ts +7 -0
- package/dist/cjs/fetchers/mongodb.d.ts.map +1 -0
- package/dist/cjs/fetchers/mongodb.js +52 -0
- package/dist/cjs/fetchers/mongodb.js.map +1 -0
- package/dist/cjs/fetchers/mysql.d.ts +3 -0
- package/dist/cjs/fetchers/mysql.d.ts.map +1 -0
- package/dist/cjs/fetchers/mysql.js +30 -0
- package/dist/cjs/fetchers/mysql.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts +3 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -0
- package/dist/cjs/fetchers/postgresql.js +25 -0
- package/dist/cjs/fetchers/postgresql.js.map +1 -0
- package/dist/cjs/fetchers/redis.d.ts +6 -0
- package/dist/cjs/fetchers/redis.d.ts.map +1 -0
- package/dist/cjs/fetchers/redis.js +46 -0
- package/dist/cjs/fetchers/redis.js.map +1 -0
- package/dist/cjs/fetchers/redshift.d.ts +2 -0
- package/dist/cjs/fetchers/redshift.d.ts.map +1 -0
- package/dist/cjs/fetchers/redshift.js +24 -0
- package/dist/cjs/fetchers/redshift.js.map +1 -0
- package/dist/cjs/fetchers/rest-api.d.ts +6 -0
- package/dist/cjs/fetchers/rest-api.d.ts.map +1 -0
- package/dist/cjs/fetchers/rest-api.js +58 -0
- package/dist/cjs/fetchers/rest-api.js.map +1 -0
- package/dist/cjs/fetchers/static-collection.d.ts +7 -0
- package/dist/cjs/fetchers/static-collection.d.ts.map +1 -0
- package/dist/cjs/fetchers/static-collection.js +24 -0
- package/dist/cjs/fetchers/static-collection.js.map +1 -0
- package/dist/cjs/fetchers/supabase.d.ts +7 -0
- package/dist/cjs/fetchers/supabase.d.ts.map +1 -0
- package/dist/cjs/fetchers/supabase.js +42 -0
- package/dist/cjs/fetchers/supabase.js.map +1 -0
- package/dist/cjs/fetchers/turso.d.ts +6 -0
- package/dist/cjs/fetchers/turso.d.ts.map +1 -0
- package/dist/cjs/fetchers/turso.js +25 -0
- package/dist/cjs/fetchers/turso.js.map +1 -0
- package/dist/cjs/index.d.ts +9 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +325 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts +5 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -0
- package/dist/cjs/pagination-plugin.js +1484 -0
- package/dist/cjs/pagination-plugin.js.map +1 -0
- package/dist/cjs/pagination-with-count.d.ts +6 -0
- package/dist/cjs/pagination-with-count.d.ts.map +1 -0
- package/dist/cjs/pagination-with-count.js +63 -0
- package/dist/cjs/pagination-with-count.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/cjs/utils.d.ts +31 -0
- package/dist/cjs/utils.d.ts.map +1 -0
- package/dist/cjs/utils.js +763 -0
- package/dist/cjs/utils.js.map +1 -0
- package/dist/cjs/validation.d.ts +5 -0
- package/dist/cjs/validation.d.ts.map +1 -0
- package/dist/cjs/validation.js +29 -0
- package/dist/cjs/validation.js.map +1 -0
- package/dist/esm/array-mapper-pagination.d.ts +32 -0
- package/dist/esm/array-mapper-pagination.d.ts.map +1 -0
- package/dist/esm/array-mapper-pagination.js +72 -0
- package/dist/esm/array-mapper-pagination.js.map +1 -0
- package/dist/esm/count-fetchers.d.ts +12 -0
- package/dist/esm/count-fetchers.d.ts.map +1 -0
- package/dist/esm/count-fetchers.js +35 -0
- package/dist/esm/count-fetchers.js.map +1 -0
- package/dist/esm/data-source-fetchers.d.ts +14 -0
- package/dist/esm/data-source-fetchers.d.ts.map +1 -0
- package/dist/esm/data-source-fetchers.js +179 -0
- package/dist/esm/data-source-fetchers.js.map +1 -0
- package/dist/esm/fetchers/airtable.d.ts +6 -0
- package/dist/esm/fetchers/airtable.d.ts.map +1 -0
- package/dist/esm/fetchers/airtable.js +22 -0
- package/dist/esm/fetchers/airtable.js.map +1 -0
- package/dist/esm/fetchers/clickhouse.d.ts +6 -0
- package/dist/esm/fetchers/clickhouse.d.ts.map +1 -0
- package/dist/esm/fetchers/clickhouse.js +24 -0
- package/dist/esm/fetchers/clickhouse.js.map +1 -0
- package/dist/esm/fetchers/csv-file.d.ts +7 -0
- package/dist/esm/fetchers/csv-file.d.ts.map +1 -0
- package/dist/esm/fetchers/csv-file.js +30 -0
- package/dist/esm/fetchers/csv-file.js.map +1 -0
- package/dist/esm/fetchers/firestore.d.ts +6 -0
- package/dist/esm/fetchers/firestore.d.ts.map +1 -0
- package/dist/esm/fetchers/firestore.js +30 -0
- package/dist/esm/fetchers/firestore.js.map +1 -0
- package/dist/esm/fetchers/google-sheets.d.ts +6 -0
- package/dist/esm/fetchers/google-sheets.d.ts.map +1 -0
- package/dist/esm/fetchers/google-sheets.js +25 -0
- package/dist/esm/fetchers/google-sheets.js.map +1 -0
- package/dist/esm/fetchers/index.d.ts +17 -0
- package/dist/esm/fetchers/index.d.ts.map +1 -0
- package/dist/esm/fetchers/index.js +17 -0
- package/dist/esm/fetchers/index.js.map +1 -0
- package/dist/esm/fetchers/javascript.d.ts +7 -0
- package/dist/esm/fetchers/javascript.d.ts.map +1 -0
- package/dist/esm/fetchers/javascript.js +34 -0
- package/dist/esm/fetchers/javascript.js.map +1 -0
- package/dist/esm/fetchers/mariadb.d.ts +3 -0
- package/dist/esm/fetchers/mariadb.d.ts.map +1 -0
- package/dist/esm/fetchers/mariadb.js +18 -0
- package/dist/esm/fetchers/mariadb.js.map +1 -0
- package/dist/esm/fetchers/mongodb.d.ts +7 -0
- package/dist/esm/fetchers/mongodb.d.ts.map +1 -0
- package/dist/esm/fetchers/mongodb.js +46 -0
- package/dist/esm/fetchers/mongodb.js.map +1 -0
- package/dist/esm/fetchers/mysql.d.ts +3 -0
- package/dist/esm/fetchers/mysql.d.ts.map +1 -0
- package/dist/esm/fetchers/mysql.js +25 -0
- package/dist/esm/fetchers/mysql.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts +3 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -0
- package/dist/esm/fetchers/postgresql.js +20 -0
- package/dist/esm/fetchers/postgresql.js.map +1 -0
- package/dist/esm/fetchers/redis.d.ts +6 -0
- package/dist/esm/fetchers/redis.d.ts.map +1 -0
- package/dist/esm/fetchers/redis.js +41 -0
- package/dist/esm/fetchers/redis.js.map +1 -0
- package/dist/esm/fetchers/redshift.d.ts +2 -0
- package/dist/esm/fetchers/redshift.d.ts.map +1 -0
- package/dist/esm/fetchers/redshift.js +20 -0
- package/dist/esm/fetchers/redshift.js.map +1 -0
- package/dist/esm/fetchers/rest-api.d.ts +6 -0
- package/dist/esm/fetchers/rest-api.d.ts.map +1 -0
- package/dist/esm/fetchers/rest-api.js +53 -0
- package/dist/esm/fetchers/rest-api.js.map +1 -0
- package/dist/esm/fetchers/static-collection.d.ts +7 -0
- package/dist/esm/fetchers/static-collection.d.ts.map +1 -0
- package/dist/esm/fetchers/static-collection.js +18 -0
- package/dist/esm/fetchers/static-collection.js.map +1 -0
- package/dist/esm/fetchers/supabase.d.ts +7 -0
- package/dist/esm/fetchers/supabase.d.ts.map +1 -0
- package/dist/esm/fetchers/supabase.js +36 -0
- package/dist/esm/fetchers/supabase.js.map +1 -0
- package/dist/esm/fetchers/turso.d.ts +6 -0
- package/dist/esm/fetchers/turso.d.ts.map +1 -0
- package/dist/esm/fetchers/turso.js +20 -0
- package/dist/esm/fetchers/turso.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +306 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts +5 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -0
- package/dist/esm/pagination-plugin.js +1457 -0
- package/dist/esm/pagination-plugin.js.map +1 -0
- package/dist/esm/pagination-with-count.d.ts +6 -0
- package/dist/esm/pagination-with-count.d.ts.map +1 -0
- package/dist/esm/pagination-with-count.js +34 -0
- package/dist/esm/pagination-with-count.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -0
- package/dist/esm/utils.d.ts +31 -0
- package/dist/esm/utils.d.ts.map +1 -0
- package/dist/esm/utils.js +722 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/validation.d.ts +5 -0
- package/dist/esm/validation.d.ts.map +1 -0
- package/dist/esm/validation.js +25 -0
- package/dist/esm/validation.js.map +1 -0
- package/package.json +33 -0
- package/src/array-mapper-pagination.ts +113 -0
- package/src/count-fetchers.ts +99 -0
- package/src/data-source-fetchers.ts +313 -0
- package/src/fetchers/airtable.ts +153 -0
- package/src/fetchers/clickhouse.ts +127 -0
- package/src/fetchers/csv-file.ts +163 -0
- package/src/fetchers/firestore.ts +138 -0
- package/src/fetchers/google-sheets.ts +189 -0
- package/src/fetchers/index.ts +32 -0
- package/src/fetchers/javascript.ts +150 -0
- package/src/fetchers/mariadb.ts +230 -0
- package/src/fetchers/mongodb.ts +239 -0
- package/src/fetchers/mysql.ts +237 -0
- package/src/fetchers/postgresql.ts +247 -0
- package/src/fetchers/redis.ts +152 -0
- package/src/fetchers/redshift.ts +138 -0
- package/src/fetchers/rest-api.ts +148 -0
- package/src/fetchers/static-collection.ts +149 -0
- package/src/fetchers/supabase.ts +246 -0
- package/src/fetchers/turso.ts +131 -0
- package/src/index.ts +352 -0
- package/src/pagination-plugin.ts +2335 -0
- package/src/pagination-with-count.ts +89 -0
- package/src/utils.ts +1013 -0
- package/src/validation.ts +32 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
# Array Mapper Pagination - Complete Implementation Guide
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
1. [Overview](#overview)
|
|
5
|
+
2. [Architecture & Design Decisions](#architecture--design-decisions)
|
|
6
|
+
3. [Critical Implementation Details](#critical-implementation-details)
|
|
7
|
+
4. [Problems Encountered & Solutions](#problems-encountered--solutions)
|
|
8
|
+
5. [UIDL Structure](#uidl-structure)
|
|
9
|
+
6. [Code Generation Flow](#code-generation-flow)
|
|
10
|
+
7. [Edge Cases & Special Handling](#edge-cases--special-handling)
|
|
11
|
+
8. [Testing & Verification](#testing--verification)
|
|
12
|
+
9. [Future Considerations](#future-considerations)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Overview
|
|
17
|
+
|
|
18
|
+
This document describes the complete implementation of server-side pagination for array mapper nodes in Next.js generated projects. The implementation supports in-memory page state management (not URL-based) and works with all data source types (JavaScript, PostgreSQL, MySQL, MongoDB, Supabase, CockroachDB, CSV, Static Collections).
|
|
19
|
+
|
|
20
|
+
### Key Features
|
|
21
|
+
- **Server-side pagination**: Each page change triggers a new data fetch with pagination parameters
|
|
22
|
+
- **In-memory state**: Page number stored in React state, not URL
|
|
23
|
+
- **Multiple paginations per page/component**: Supports multiple independent paginated lists
|
|
24
|
+
- **Same data source, different perPage**: Same data source can be used with different pagination settings
|
|
25
|
+
- **Mixed paginated/non-paginated**: Same data source can be used both paginated and non-paginated on the same page
|
|
26
|
+
- **Deduplication**: Count fetches and useEffect calls are deduplicated per data source
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Architecture & Design Decisions
|
|
31
|
+
|
|
32
|
+
### 1. Why Server-Side Pagination?
|
|
33
|
+
|
|
34
|
+
**Initial Mistake**: The first implementation used client-side array slicing (fetching all data then slicing in the browser). This was incorrect.
|
|
35
|
+
|
|
36
|
+
**Correct Approach**:
|
|
37
|
+
- For database sources: Pass `page` and `perPage` to the database query (OFFSET/LIMIT, skip/take)
|
|
38
|
+
- For JavaScript sources: Apply pagination in the API handler before returning data
|
|
39
|
+
- For all sources: Fetch only the data needed for the current page
|
|
40
|
+
|
|
41
|
+
### 2. Why In-Memory State (Not URL-Based)?
|
|
42
|
+
|
|
43
|
+
The CMS collection node already uses URL-based pagination (`?page=1&perPage=10`). For array mappers within a page, we needed a different approach:
|
|
44
|
+
- Allows multiple independent paginated lists on one page
|
|
45
|
+
- No URL pollution or conflicts
|
|
46
|
+
- Simpler state management
|
|
47
|
+
- Better for nested/dynamic content
|
|
48
|
+
|
|
49
|
+
### 3. Why Two Separate Implementations (Pages vs Components)?
|
|
50
|
+
|
|
51
|
+
**Pages with `getStaticProps`**:
|
|
52
|
+
- Initial data fetched server-side
|
|
53
|
+
- Total count fetched server-side
|
|
54
|
+
- `maxPages` calculated in `getStaticProps` and passed as props
|
|
55
|
+
- Client-side navigation triggers new fetches via DataProvider
|
|
56
|
+
|
|
57
|
+
**Components (no `getStaticProps`)**:
|
|
58
|
+
- Initial data fetched on mount via DataProvider
|
|
59
|
+
- Total count fetched separately via API route using `useEffect`
|
|
60
|
+
- `maxPages` calculated client-side from count response
|
|
61
|
+
- Each page change triggers DataProvider re-fetch
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Critical Implementation Details
|
|
66
|
+
|
|
67
|
+
### 1. The Plugin Execution Order Problem
|
|
68
|
+
|
|
69
|
+
**Critical Discovery**: UIDL elements are transformed **before** custom plugins run.
|
|
70
|
+
|
|
71
|
+
**The Problem**:
|
|
72
|
+
```typescript
|
|
73
|
+
// UIDL has this:
|
|
74
|
+
{ type: 'cms-pagination-node', ... }
|
|
75
|
+
{ type: 'cms-navigation-button', name: 'Previous', ... }
|
|
76
|
+
|
|
77
|
+
// But when our pagination plugin runs, it's already transformed to:
|
|
78
|
+
<div className="home-cms-pagination-node1">
|
|
79
|
+
<div className="home-previous1">Previous</div>
|
|
80
|
+
<div className="home-next1">Next</div>
|
|
81
|
+
</div>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Solution**: We cannot rely on UIDL node types in the pagination plugin. Instead, we traverse the **generated JSX AST** and identify elements by their **class names**:
|
|
85
|
+
- Pagination containers: `className.includes('cms-pagination-node')`
|
|
86
|
+
- Navigation buttons: `className.includes('previous')` or `className.includes('next')`
|
|
87
|
+
|
|
88
|
+
**Files Involved**:
|
|
89
|
+
- `src/pagination-plugin.ts`: `detectPaginationsFromJSX()` function
|
|
90
|
+
|
|
91
|
+
### 2. The perPage Extraction Timing Problem
|
|
92
|
+
|
|
93
|
+
**The Problem**: We need to read `perPage` from the UIDL, but it gets stripped during validation or transformation.
|
|
94
|
+
|
|
95
|
+
**First Attempt**: Read `perPage` in the pagination plugin → Failed, already transformed
|
|
96
|
+
|
|
97
|
+
**Second Attempt**: Read `perPage` from transformed nodes → Not available
|
|
98
|
+
|
|
99
|
+
**Third Attempt**: Add `perPage` to UIDL type definitions → Validator stripped it anyway
|
|
100
|
+
|
|
101
|
+
**Solution (Multi-Step)**:
|
|
102
|
+
|
|
103
|
+
#### Step 1: Update UIDL Type Definitions
|
|
104
|
+
```typescript
|
|
105
|
+
// packages/teleport-types/src/uidl.ts
|
|
106
|
+
export interface UIDLCMSListRepeaterNodeContent {
|
|
107
|
+
// ... existing fields
|
|
108
|
+
paginated?: boolean // NEW
|
|
109
|
+
perPage?: number // NEW
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Step 2: Update UIDL Validator Schema
|
|
114
|
+
```typescript
|
|
115
|
+
// packages/teleport-uidl-validator/src/decoders/utils.ts
|
|
116
|
+
export const cmsListRepeaterNodeDecoder = object({
|
|
117
|
+
// ... existing fields
|
|
118
|
+
paginated: optional(boolean()), // NEW
|
|
119
|
+
perPage: optional(number()), // NEW
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### Step 3: Early Extraction (Before Any Transformations)
|
|
124
|
+
```typescript
|
|
125
|
+
// packages/teleport-plugin-next-data-source/src/index.ts
|
|
126
|
+
function extractPerPageValuesEarly(uidlNode: any): Map<string, number> {
|
|
127
|
+
// This runs BEFORE any transformations
|
|
128
|
+
// Traverses raw UIDL and extracts perPage values
|
|
129
|
+
// Stores them in a Map keyed by renderPropIdentifier
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Why renderPropIdentifier**: Initially keyed by data source identifier, but this caused issues when the same data source was used multiple times with different `perPage` values. The `renderPropIdentifier` is unique per array mapper instance.
|
|
134
|
+
|
|
135
|
+
**Traversal Complexity**: The UIDL structure is deeply nested and inconsistent:
|
|
136
|
+
```typescript
|
|
137
|
+
// Must traverse ALL of these structures:
|
|
138
|
+
node.content.children[] // Regular children
|
|
139
|
+
node.content.node // Single child
|
|
140
|
+
node.content.nodes.success // DataProvider success state
|
|
141
|
+
node.content.nodes.error // DataProvider error state
|
|
142
|
+
node.content.nodes.loading // DataProvider loading state
|
|
143
|
+
node.content.nodes.list // cms-list-repeater list state
|
|
144
|
+
node.content.nodes.empty // cms-list-repeater empty state
|
|
145
|
+
node.children[] // Direct children array
|
|
146
|
+
// Conditional nodes have different structures too
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 3. The DataProvider Fetch Problem
|
|
150
|
+
|
|
151
|
+
**The Problem**: When we set `initialData` on DataProvider, the `fetchData` function never gets called, even when params change.
|
|
152
|
+
|
|
153
|
+
**Why**: DataProvider has an internal ref `passFetchBecauseWeHaveInitialData`:
|
|
154
|
+
```javascript
|
|
155
|
+
// Inside DataProvider implementation
|
|
156
|
+
const passFetchBecauseWeHaveInitialData = useRef(false)
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (initialData) {
|
|
160
|
+
passFetchBecauseWeHaveInitialData.current = true
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
// ... fetch logic
|
|
164
|
+
}, [initialData, params])
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Once `initialData` is provided, this ref is set to `true` and never resets when `params` change.
|
|
168
|
+
|
|
169
|
+
**Solution (Three Parts)**:
|
|
170
|
+
|
|
171
|
+
#### Part 1: Conditional initialData
|
|
172
|
+
```javascript
|
|
173
|
+
// Only provide initialData for page 1
|
|
174
|
+
initialData={pagination_pg_0_page === 1 ? props?.dataSource_pg_0 : undefined}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Part 2: Add Key Prop to Force Remount
|
|
178
|
+
```javascript
|
|
179
|
+
// Force React to remount DataProvider when page changes
|
|
180
|
+
key={`dataSource-page-${pagination_pg_0_page}`}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### Part 3: Ensure fetchData Accepts and Uses Params
|
|
184
|
+
```javascript
|
|
185
|
+
fetchData={(params) =>
|
|
186
|
+
fetch(`/api/data-source?${new URLSearchParams(params).toString()}`)
|
|
187
|
+
.then((res) => res.json())
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### 4. The Multiple DataProvider Instance Problem
|
|
192
|
+
|
|
193
|
+
**The Problem**: When the same data source is used multiple times (some paginated, some not), `getStaticProps` had only one fetch per data source. This meant:
|
|
194
|
+
```javascript
|
|
195
|
+
// In getStaticProps (WRONG):
|
|
196
|
+
const [test_users_data] = await Promise.all([
|
|
197
|
+
fetchData({ page: 1, perPage: 10 }) // Only fetches 10 items
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
// Then in the component:
|
|
201
|
+
// Paginated DataProvider - works, gets items 0-9
|
|
202
|
+
<DataProvider name="test_users_data" initialData={props.test_users_data}>
|
|
203
|
+
{test_users_data?.[0]} // ✅ Works
|
|
204
|
+
</DataProvider>
|
|
205
|
+
|
|
206
|
+
// Non-paginated DataProvider - BROKEN
|
|
207
|
+
<DataProvider name="test_users_data" initialData={props.test_users_data}>
|
|
208
|
+
{test_users_data?.[1]} // ❌ undefined! Only 10 items fetched
|
|
209
|
+
</DataProvider>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Solution**: Generate separate `fetchData` calls for each DataProvider instance in `getStaticProps`:
|
|
213
|
+
```javascript
|
|
214
|
+
const [
|
|
215
|
+
test_users_data, // Full data, no params
|
|
216
|
+
test_users_data_pg_0, // Paginated data, page 1, perPage 10
|
|
217
|
+
test_users_data_pg_1, // Paginated data, page 1, perPage 5
|
|
218
|
+
] = await Promise.all([
|
|
219
|
+
fetchData(), // For non-paginated DataProvider
|
|
220
|
+
fetchData({ page: 1, perPage: 10 }), // For first paginated DataProvider
|
|
221
|
+
fetchData({ page: 1, perPage: 5 }), // For second paginated DataProvider
|
|
222
|
+
])
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Implementation Detail**: We traverse the JSX AST to identify all DataProvider instances, not just unique data sources.
|
|
226
|
+
|
|
227
|
+
### 5. The fetchData Creation Problem
|
|
228
|
+
|
|
229
|
+
**The Problem**: When a DataProvider is static (data source only used in `getStaticProps`), it doesn't get a `fetchData` prop. But pagination requires `fetchData` for page changes.
|
|
230
|
+
|
|
231
|
+
**Solution**: If `fetchData` doesn't exist on a paginated DataProvider, we create it:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// src/pagination-plugin.ts - addPaginationParamsToDataProvider()
|
|
235
|
+
if (!existingFetchDataAttr) {
|
|
236
|
+
// DataProvider doesn't have fetchData, create it
|
|
237
|
+
const resourceDefAttr = dataProviderJSX.openingElement.attributes.find(
|
|
238
|
+
(attr: any) => attr.type === 'JSXAttribute' && attr.name.name === 'resourceDefinition'
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if (resourceDefAttr) {
|
|
242
|
+
const resourceDef = // ... extract from AST
|
|
243
|
+
const apiPath = `/api/${dataSourceType}-${tableName}-${dataSourceId.substring(0, 8)}`
|
|
244
|
+
|
|
245
|
+
const newFetchData = types.arrowFunctionExpression(
|
|
246
|
+
[types.identifier('params')],
|
|
247
|
+
// ... create fetch call with params
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
dataProviderJSX.openingElement.attributes.push(
|
|
251
|
+
types.jsxAttribute(
|
|
252
|
+
types.jsxIdentifier('fetchData'),
|
|
253
|
+
types.jsxExpressionContainer(newFetchData)
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Also Required**: Ensure API routes are generated by explicitly calling `extractDataSourceIntoNextAPIFolder()` for paginated data sources, even if they're static.
|
|
261
|
+
|
|
262
|
+
### 6. The Count Fetching Architecture
|
|
263
|
+
|
|
264
|
+
**Wrong Approach**: Return `total` count from the main data handler:
|
|
265
|
+
```javascript
|
|
266
|
+
// WRONG - couples data fetching with count
|
|
267
|
+
return {
|
|
268
|
+
success: true,
|
|
269
|
+
data: results,
|
|
270
|
+
total: 1000 // ❌ Don't do this
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Correct Approach**: Separate `getCount` handler:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// utils/data-sources/data-source-name.js
|
|
278
|
+
export async function handler(req, res) {
|
|
279
|
+
// Fetch data only
|
|
280
|
+
return res.status(200).json({
|
|
281
|
+
success: true,
|
|
282
|
+
data: results
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function getCount(req, res) {
|
|
287
|
+
// Fetch count only, handle filters/queries
|
|
288
|
+
const { query, queryColumns, filters } = req.query
|
|
289
|
+
// ... apply same filters as handler
|
|
290
|
+
return res.status(200).json({
|
|
291
|
+
success: true,
|
|
292
|
+
count: total
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Why Separate**:
|
|
298
|
+
- Clean separation of concerns
|
|
299
|
+
- Can fetch count without data
|
|
300
|
+
- Easier to optimize queries (COUNT vs SELECT)
|
|
301
|
+
- Same handler signature for consistency
|
|
302
|
+
|
|
303
|
+
**Wrapper Functions**:
|
|
304
|
+
```typescript
|
|
305
|
+
// Also in utils/data-sources/data-source-name.js
|
|
306
|
+
async function fetchData(params = {}) {
|
|
307
|
+
// Wrapper for server-side calls, simulates req/res
|
|
308
|
+
const req = { query: params, method: 'GET' }
|
|
309
|
+
const res = { /* mock res */ }
|
|
310
|
+
await handler(req, res)
|
|
311
|
+
return result.data
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function fetchCount(params = {}) {
|
|
315
|
+
// Wrapper for server-side calls
|
|
316
|
+
const req = { query: params, method: 'GET' }
|
|
317
|
+
const res = { /* mock res */ }
|
|
318
|
+
await getCount(req, res)
|
|
319
|
+
return result.count
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export { fetchData, fetchCount, handler, getCount }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Count API Routes**: For components, create separate count endpoints:
|
|
326
|
+
```javascript
|
|
327
|
+
// pages/api/data-source-name-count.js
|
|
328
|
+
import dataSource from '../../utils/data-sources/data-source-name'
|
|
329
|
+
export default dataSource.getCount
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Problems Encountered & Solutions
|
|
335
|
+
|
|
336
|
+
### Problem 1: getStaticProps Parsing Complexity
|
|
337
|
+
|
|
338
|
+
**Issue**: Need to inject `fetchCount()` calls into existing `Promise.all` in `getStaticProps`.
|
|
339
|
+
|
|
340
|
+
**Complexity**:
|
|
341
|
+
```typescript
|
|
342
|
+
// Original code structure:
|
|
343
|
+
const [data1, data2] = await Promise.all([
|
|
344
|
+
import1.fetchData().catch(error => { /* ... */ }),
|
|
345
|
+
import2.fetchData().catch(error => { /* ... */ }),
|
|
346
|
+
])
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Challenge**: The `CallExpression` is wrapped in `.catch()`, making AST traversal tricky.
|
|
350
|
+
|
|
351
|
+
**Solution**:
|
|
352
|
+
```typescript
|
|
353
|
+
// Detect fetchData calls even when wrapped
|
|
354
|
+
if (element.callee?.type === 'MemberExpression' &&
|
|
355
|
+
element.callee?.property?.name === 'catch' &&
|
|
356
|
+
element.callee?.object?.type === 'CallExpression') {
|
|
357
|
+
fetchCallExpr = element.callee.object // Unwrap
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Now we can access the actual fetchData call
|
|
361
|
+
if (fetchCallExpr.callee?.type === 'MemberExpression' &&
|
|
362
|
+
fetchCallExpr.callee?.property?.name === 'fetchData') {
|
|
363
|
+
const importName = fetchCallExpr.callee.object.name
|
|
364
|
+
// Map import to data source variable
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Problem 2: Variable Naming Collisions
|
|
369
|
+
|
|
370
|
+
**Issue**: When same data source used multiple times, generated this:
|
|
371
|
+
```javascript
|
|
372
|
+
const [
|
|
373
|
+
test_users_data,
|
|
374
|
+
test_users_data_count, // For pagination 1
|
|
375
|
+
test_users_data_count, // For pagination 2 - COLLISION!
|
|
376
|
+
] = await Promise.all([...])
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
**Solution**:
|
|
380
|
+
- **Count variables**: Deduplicate by data source (only one count needed per source)
|
|
381
|
+
- **Data variables**: Unique per pagination instance: `${dataSource}_pg_${index}`
|
|
382
|
+
- **maxPages variables**: Unique per pagination instance: `${dataSource}_pg_${index}_maxPages`
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
const [
|
|
386
|
+
test_users_data,
|
|
387
|
+
test_users_data_pg_0,
|
|
388
|
+
test_users_data_pg_1,
|
|
389
|
+
test_users_data_count, // Only one count
|
|
390
|
+
] = await Promise.all([
|
|
391
|
+
fetchData(),
|
|
392
|
+
fetchData({ page: 1, perPage: 10 }),
|
|
393
|
+
fetchData({ page: 1, perPage: 5 }),
|
|
394
|
+
fetchCount(), // Only one count call
|
|
395
|
+
])
|
|
396
|
+
|
|
397
|
+
// Then calculate separate maxPages
|
|
398
|
+
const test_users_data_pg_0_maxPages = Math.ceil(test_users_data_count / 10)
|
|
399
|
+
const test_users_data_pg_1_maxPages = Math.ceil(test_users_data_count / 5)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Problem 3: Button Disabled States
|
|
403
|
+
|
|
404
|
+
**Initial Mistake**:
|
|
405
|
+
```javascript
|
|
406
|
+
disabled={!hasPrevPage} // Requires calculating hasPrevPage
|
|
407
|
+
disabled={!hasNextPage} // Requires calculating hasNextPage
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Better Approach**:
|
|
411
|
+
```javascript
|
|
412
|
+
disabled={page <= 1} // Simple, direct
|
|
413
|
+
disabled={page >= maxPages} // Simple, direct
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Why**:
|
|
417
|
+
- No need to maintain separate `hasPrevPage`/`hasNextPage` state
|
|
418
|
+
- Less state to manage
|
|
419
|
+
- Clearer logic
|
|
420
|
+
- Pages are 1-indexed (page 1 is first page)
|
|
421
|
+
|
|
422
|
+
### Problem 4: API Routes Not Generated
|
|
423
|
+
|
|
424
|
+
**Issue**: For static data sources (only used in `getStaticProps`), no API routes were created. But pagination needs API routes for client-side fetching.
|
|
425
|
+
|
|
426
|
+
**Root Cause**: The `extractDataSourceIntoNextAPIFolder` function was only called during UIDL traversal for data-source-list nodes. Static sources weren't traversed this way.
|
|
427
|
+
|
|
428
|
+
**Solution**: In the pagination plugin, explicitly call `extractDataSourceIntoNextAPIFolder` for each paginated data source:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// src/pagination-plugin.ts
|
|
432
|
+
function createAPIRoutesForPaginatedDataSources(...) {
|
|
433
|
+
const paginatedDataSourceIds = new Set(
|
|
434
|
+
paginationInfos.map((info) => info.dataSourceIdentifier)
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
// Traverse UIDL to find paginated data sources
|
|
438
|
+
const traverseForDataSources = (node: any): void => {
|
|
439
|
+
if (node.type === 'data-source-list' || node.type === 'data-source-item') {
|
|
440
|
+
const renderProp = node.content.renderPropIdentifier
|
|
441
|
+
if (renderProp && paginatedDataSourceIds.has(renderProp)) {
|
|
442
|
+
// Force API route creation
|
|
443
|
+
extractDataSourceIntoNextAPIFolder(
|
|
444
|
+
node,
|
|
445
|
+
dataSources,
|
|
446
|
+
componentChunk,
|
|
447
|
+
extractedResources
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
// Also create count API route for components
|
|
451
|
+
if (isComponent) {
|
|
452
|
+
extractedResources[`api/${fileName}-count`] = {
|
|
453
|
+
fileName: `${fileName}-count`,
|
|
454
|
+
fileType: FileType.JS,
|
|
455
|
+
path: ['pages', 'api'],
|
|
456
|
+
content: `import dataSource from '../../utils/data-sources/${fileName}'
|
|
457
|
+
export default dataSource.getCount`
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Problem 5: Duplicate persistDataDuringLoading
|
|
467
|
+
|
|
468
|
+
**Issue**: DataProvider received duplicate `persistDataDuringLoading={true}` attributes.
|
|
469
|
+
|
|
470
|
+
**Root Cause**: Both `extractDataSourceIntoNextAPIFolder` and `extractDataSourceIntoGetStaticProps` were adding the attribute.
|
|
471
|
+
|
|
472
|
+
**Solution**: Check if attribute exists before adding:
|
|
473
|
+
```typescript
|
|
474
|
+
const existingPersistAttr = jsxNode.openingElement.attributes.find(
|
|
475
|
+
(attr: any) => attr.type === 'JSXAttribute' &&
|
|
476
|
+
attr.name.name === 'persistDataDuringLoading'
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
if (!existingPersistAttr) {
|
|
480
|
+
jsxNode.openingElement.attributes.push(
|
|
481
|
+
types.jsxAttribute(
|
|
482
|
+
types.jsxIdentifier('persistDataDuringLoading'),
|
|
483
|
+
types.jsxExpressionContainer(types.booleanLiteral(true))
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Problem 6: Component Count Fetching Deduplication
|
|
490
|
+
|
|
491
|
+
**Issue**: In components, when same data source used in multiple paginated lists, we were creating separate `useEffect` calls for each:
|
|
492
|
+
|
|
493
|
+
```javascript
|
|
494
|
+
// INEFFICIENT
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
fetch('/api/data-source-count')
|
|
497
|
+
.then(res => res.json())
|
|
498
|
+
.then(data => setPagination_pg_0_maxPages(Math.ceil(data.count / 10)))
|
|
499
|
+
}, [])
|
|
500
|
+
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
fetch('/api/data-source-count') // Same fetch!
|
|
503
|
+
.then(res => res.json())
|
|
504
|
+
.then(data => setPagination_pg_1_maxPages(Math.ceil(data.count / 5)))
|
|
505
|
+
}, [])
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**Solution**: Group by data source, one `useEffect` per unique source:
|
|
509
|
+
|
|
510
|
+
```javascript
|
|
511
|
+
// EFFICIENT
|
|
512
|
+
useEffect(() => {
|
|
513
|
+
fetch('/api/data-source-count')
|
|
514
|
+
.then(res => res.json())
|
|
515
|
+
.then(data => {
|
|
516
|
+
setPagination_pg_0_maxPages(Math.ceil(data.count / 10))
|
|
517
|
+
setPagination_pg_1_maxPages(Math.ceil(data.count / 5))
|
|
518
|
+
})
|
|
519
|
+
}, [])
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
**Implementation**:
|
|
523
|
+
```typescript
|
|
524
|
+
// Group paginationInfos by fileName (data source)
|
|
525
|
+
const dataSourceToInfos = new Map<string, typeof paginationInfos>()
|
|
526
|
+
paginationInfos.forEach((info) => {
|
|
527
|
+
const key = (info as any).fileName
|
|
528
|
+
if (!dataSourceToInfos.has(key)) {
|
|
529
|
+
dataSourceToInfos.set(key, [])
|
|
530
|
+
}
|
|
531
|
+
dataSourceToInfos.get(key)!.push(info)
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// Create ONE useEffect per unique data source
|
|
535
|
+
dataSourceToInfos.forEach((infos) => {
|
|
536
|
+
const { fileName } = infos[0]
|
|
537
|
+
const countApiPath = `/api/${fileName}-count`
|
|
538
|
+
|
|
539
|
+
const setStateStatements = infos.map((info) =>
|
|
540
|
+
types.expressionStatement(
|
|
541
|
+
types.callExpression(
|
|
542
|
+
types.identifier((info as any).setMaxPagesStateVar),
|
|
543
|
+
[types.callExpression(
|
|
544
|
+
types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
|
|
545
|
+
[types.binaryExpression('/',
|
|
546
|
+
types.memberExpression(types.identifier('data'), types.identifier('count')),
|
|
547
|
+
types.numericLiteral(info.perPage)
|
|
548
|
+
)]
|
|
549
|
+
)]
|
|
550
|
+
)
|
|
551
|
+
)
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
// Generate single useEffect with multiple setState calls
|
|
555
|
+
})
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## UIDL Structure
|
|
561
|
+
|
|
562
|
+
### Array Mapper with Pagination
|
|
563
|
+
|
|
564
|
+
```json
|
|
565
|
+
{
|
|
566
|
+
"type": "cms-list-repeater",
|
|
567
|
+
"content": {
|
|
568
|
+
"renderPropIdentifier": "context_abc123",
|
|
569
|
+
"source": "test_users_data[0]",
|
|
570
|
+
"paginated": true,
|
|
571
|
+
"perPage": 10,
|
|
572
|
+
"nodes": {
|
|
573
|
+
"list": {
|
|
574
|
+
"type": "element",
|
|
575
|
+
"content": {
|
|
576
|
+
"elementType": "div",
|
|
577
|
+
"children": [
|
|
578
|
+
{
|
|
579
|
+
"type": "dynamic",
|
|
580
|
+
"content": {
|
|
581
|
+
"referenceType": "local",
|
|
582
|
+
"id": "context_abc123"
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
]
|
|
586
|
+
}
|
|
587
|
+
},
|
|
588
|
+
"empty": null
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### Pagination Container with Buttons
|
|
595
|
+
|
|
596
|
+
```json
|
|
597
|
+
{
|
|
598
|
+
"type": "element",
|
|
599
|
+
"content": {
|
|
600
|
+
"elementType": "cms-pagination-node",
|
|
601
|
+
"children": [
|
|
602
|
+
{
|
|
603
|
+
"type": "element",
|
|
604
|
+
"content": {
|
|
605
|
+
"elementType": "cms-navigation-button",
|
|
606
|
+
"name": "Previous",
|
|
607
|
+
"children": [
|
|
608
|
+
{
|
|
609
|
+
"type": "static",
|
|
610
|
+
"content": "Previous"
|
|
611
|
+
}
|
|
612
|
+
]
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
"type": "element",
|
|
617
|
+
"content": {
|
|
618
|
+
"elementType": "cms-navigation-button",
|
|
619
|
+
"name": "Next",
|
|
620
|
+
"children": [
|
|
621
|
+
{
|
|
622
|
+
"type": "static",
|
|
623
|
+
"content": "Next"
|
|
624
|
+
}
|
|
625
|
+
]
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
]
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Important**: Pagination node is a direct sibling of the data source node, not nested inside it.
|
|
634
|
+
|
|
635
|
+
---
|
|
636
|
+
|
|
637
|
+
## Code Generation Flow
|
|
638
|
+
|
|
639
|
+
### 1. Early Extraction Phase (`index.ts`)
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// RUNS FIRST, before any transformations
|
|
643
|
+
const perPageMap = extractPerPageValuesEarly(uidl.node)
|
|
644
|
+
|
|
645
|
+
// Store in options for later use
|
|
646
|
+
if (!options.paginationConfig) {
|
|
647
|
+
options.paginationConfig = { perPageMap: new Map() }
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Merge maps (important for multiple component generations)
|
|
651
|
+
for (const [key, value] of perPageMap.entries()) {
|
|
652
|
+
options.paginationConfig.perPageMap.set(key, value)
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### 2. Data Source Plugin Execution
|
|
657
|
+
|
|
658
|
+
Standard data source processing happens (creates DataProviders, utils files, etc.)
|
|
659
|
+
|
|
660
|
+
### 3. Pagination Plugin Execution (`pagination-plugin.ts`)
|
|
661
|
+
|
|
662
|
+
#### Step 3.1: Detect Paginations from JSX
|
|
663
|
+
```typescript
|
|
664
|
+
const detectedPaginations = detectPaginationsFromJSX(blockStatement, uidl.node)
|
|
665
|
+
// Returns: [
|
|
666
|
+
// {
|
|
667
|
+
// paginationNodeClass: 'home-cms-pagination-node1',
|
|
668
|
+
// prevButtonClass: 'home-previous1',
|
|
669
|
+
// nextButtonClass: 'home-next1',
|
|
670
|
+
// dataSourceIdentifier: 'test_users_data',
|
|
671
|
+
// dataProviderJSX: <DataProvider JSX node>,
|
|
672
|
+
// arrayMapperRenderProp: 'context_abc123'
|
|
673
|
+
// }
|
|
674
|
+
// ]
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
#### Step 3.2: Generate Pagination Logic
|
|
678
|
+
```typescript
|
|
679
|
+
detectedPaginations.forEach((detected, index) => {
|
|
680
|
+
const paginationNodeId = `pg_${index}`
|
|
681
|
+
const lookupKey = detected.arrayMapperRenderProp || detected.dataSourceIdentifier
|
|
682
|
+
const perPage = perPageMap.get(lookupKey) || 10
|
|
683
|
+
|
|
684
|
+
const info = generatePaginationLogic(paginationNodeId, detected.dataSourceIdentifier, perPage)
|
|
685
|
+
paginationInfos.push(info)
|
|
686
|
+
|
|
687
|
+
// Add state declarations to component
|
|
688
|
+
// pagination_pg_0_page, setPagination_pg_0_page, etc.
|
|
689
|
+
})
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
#### Step 3.3: Create API Routes (if needed)
|
|
693
|
+
```typescript
|
|
694
|
+
createAPIRoutesForPaginatedDataSources(...)
|
|
695
|
+
// - Ensures API routes exist for static data sources
|
|
696
|
+
// - Creates count API routes for components
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
#### Step 3.4: Add Pagination Params to DataProviders
|
|
700
|
+
```typescript
|
|
701
|
+
detectedPaginations.forEach((detected, index) => {
|
|
702
|
+
addPaginationParamsToDataProvider(detected.dataProviderJSX, paginationInfos[index], index)
|
|
703
|
+
// - Adds params prop
|
|
704
|
+
// - Modifies or creates fetchData
|
|
705
|
+
// - Sets initialData conditionally
|
|
706
|
+
// - Adds key prop
|
|
707
|
+
})
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
#### Step 3.5: Modify Navigation Buttons
|
|
711
|
+
```typescript
|
|
712
|
+
modifyPaginationButtons(blockStatement, detectedPaginations, paginationInfos)
|
|
713
|
+
// - Changes div to button
|
|
714
|
+
// - Adds onClick handlers
|
|
715
|
+
// - Adds disabled attributes
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
#### Step 3.6: Modify getStaticProps (Pages Only)
|
|
719
|
+
```typescript
|
|
720
|
+
if (isPage) {
|
|
721
|
+
modifyGetStaticPropsForPagination(chunks, paginationInfos)
|
|
722
|
+
// - Add fetchData calls for each pagination
|
|
723
|
+
// - Add fetchCount calls (deduplicated)
|
|
724
|
+
// - Calculate maxPages
|
|
725
|
+
// - Add to props
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
#### Step 3.7: Add useEffect for Count (Components Only)
|
|
730
|
+
```typescript
|
|
731
|
+
if (isComponent) {
|
|
732
|
+
// Add useEffect calls (deduplicated by data source)
|
|
733
|
+
// Fetch count from API
|
|
734
|
+
// Calculate and set maxPages
|
|
735
|
+
}
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### 4. Generated Code Structure
|
|
739
|
+
|
|
740
|
+
#### Page (with getStaticProps):
|
|
741
|
+
```javascript
|
|
742
|
+
'use client'
|
|
743
|
+
import { useState } from 'react'
|
|
744
|
+
import { DataProvider } from '@teleporthq/react-components'
|
|
745
|
+
import dataSource1 from '../utils/data-sources/javascript-data-34a5011c'
|
|
746
|
+
import dataSource2 from '../utils/data-sources/cockroachdb-users-4b96921b'
|
|
747
|
+
|
|
748
|
+
const Home = (props) => {
|
|
749
|
+
const [pagination_pg_0_page, setPagination_pg_0_page] = useState(1)
|
|
750
|
+
const [pagination_pg_0_maxPages, setPagination_pg_0_maxPages] = useState(
|
|
751
|
+
props?.test1_data_data_pg_0_maxPages || 0
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
return (
|
|
755
|
+
<div>
|
|
756
|
+
<DataProvider
|
|
757
|
+
name="test1_data_data"
|
|
758
|
+
initialData={pagination_pg_0_page === 1 ? props?.test1_data_data_pg_0 : undefined}
|
|
759
|
+
params={{ page: pagination_pg_0_page, perPage: 3 }}
|
|
760
|
+
fetchData={(params) =>
|
|
761
|
+
fetch(`/api/javascript-data-34a5011c?${new URLSearchParams(params).toString()}`)
|
|
762
|
+
.then((res) => res.json())
|
|
763
|
+
.then((data) => data.data)
|
|
764
|
+
}
|
|
765
|
+
persistDataDuringLoading={true}
|
|
766
|
+
key={`test1_data_data-page-${pagination_pg_0_page}`}
|
|
767
|
+
>
|
|
768
|
+
{/* Array mapper content */}
|
|
769
|
+
</DataProvider>
|
|
770
|
+
|
|
771
|
+
<div className="home-cms-pagination-node1">
|
|
772
|
+
<button
|
|
773
|
+
type="button"
|
|
774
|
+
className="home-previous1"
|
|
775
|
+
onClick={() => setPagination_pg_0_page((p) => Math.max(1, p - 1))}
|
|
776
|
+
disabled={pagination_pg_0_page <= 1}
|
|
777
|
+
>
|
|
778
|
+
Previous
|
|
779
|
+
</button>
|
|
780
|
+
<button
|
|
781
|
+
type="button"
|
|
782
|
+
className="home-next1"
|
|
783
|
+
onClick={() => setPagination_pg_0_page((p) => p + 1)}
|
|
784
|
+
disabled={pagination_pg_0_page >= pagination_pg_0_maxPages}
|
|
785
|
+
>
|
|
786
|
+
Next
|
|
787
|
+
</button>
|
|
788
|
+
</div>
|
|
789
|
+
</div>
|
|
790
|
+
)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export async function getStaticProps() {
|
|
794
|
+
try {
|
|
795
|
+
const [
|
|
796
|
+
test1_data_data, // Non-paginated
|
|
797
|
+
test1_data_data_pg_0, // Paginated instance 1
|
|
798
|
+
test1_data_data_pg_1, // Paginated instance 2
|
|
799
|
+
test_users_data, // Non-paginated
|
|
800
|
+
test_users_data_pg_2, // Paginated instance 3
|
|
801
|
+
test1_data_data_count, // Count for test1_data_data
|
|
802
|
+
test_users_data_count, // Count for test_users_data
|
|
803
|
+
] = await Promise.all([
|
|
804
|
+
dataSource1.fetchData().catch(() => []),
|
|
805
|
+
dataSource1.fetchData({ page: 1, perPage: 3 }).catch(() => []),
|
|
806
|
+
dataSource1.fetchData({ page: 1, perPage: 5 }).catch(() => []),
|
|
807
|
+
dataSource2.fetchData().catch(() => []),
|
|
808
|
+
dataSource2.fetchData({ page: 1, perPage: 10 }).catch(() => []),
|
|
809
|
+
dataSource1.fetchCount(),
|
|
810
|
+
dataSource2.fetchCount(),
|
|
811
|
+
])
|
|
812
|
+
|
|
813
|
+
const test1_data_data_pg_0_maxPages = Math.ceil(test1_data_data_count / 3)
|
|
814
|
+
const test1_data_data_pg_1_maxPages = Math.ceil(test1_data_data_count / 5)
|
|
815
|
+
const test_users_data_pg_2_maxPages = Math.ceil(test_users_data_count / 10)
|
|
816
|
+
|
|
817
|
+
return {
|
|
818
|
+
props: {
|
|
819
|
+
test1_data_data,
|
|
820
|
+
test1_data_data_pg_0,
|
|
821
|
+
test1_data_data_pg_1,
|
|
822
|
+
test_users_data,
|
|
823
|
+
test_users_data_pg_2,
|
|
824
|
+
test1_data_data_pg_0_maxPages,
|
|
825
|
+
test1_data_data_pg_1_maxPages,
|
|
826
|
+
test_users_data_pg_2_maxPages,
|
|
827
|
+
},
|
|
828
|
+
revalidate: 60,
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
return { props: {} }
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export default Home
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
#### Component (without getStaticProps):
|
|
839
|
+
```javascript
|
|
840
|
+
'use client'
|
|
841
|
+
import { useState, useEffect } from 'react'
|
|
842
|
+
import { DataProvider } from '@teleporthq/react-components'
|
|
843
|
+
|
|
844
|
+
const MyComponent = (props) => {
|
|
845
|
+
const [pagination_pg_0_page, setPagination_pg_0_page] = useState(1)
|
|
846
|
+
const [pagination_pg_0_maxPages, setPagination_pg_0_maxPages] = useState(0)
|
|
847
|
+
|
|
848
|
+
const [pagination_pg_1_page, setPagination_pg_1_page] = useState(1)
|
|
849
|
+
const [pagination_pg_1_maxPages, setPagination_pg_1_maxPages] = useState(0)
|
|
850
|
+
|
|
851
|
+
// One useEffect per unique data source
|
|
852
|
+
useEffect(() => {
|
|
853
|
+
fetch('/api/javascript-data-34a5011c-count')
|
|
854
|
+
.then((res) => res.json())
|
|
855
|
+
.then((data) => {
|
|
856
|
+
setPagination_pg_0_maxPages(Math.ceil(data.count / 3))
|
|
857
|
+
setPagination_pg_1_maxPages(Math.ceil(data.count / 5))
|
|
858
|
+
})
|
|
859
|
+
.catch((error) => console.error('Error fetching count:', error))
|
|
860
|
+
}, [])
|
|
861
|
+
|
|
862
|
+
return (
|
|
863
|
+
<div>
|
|
864
|
+
<DataProvider
|
|
865
|
+
name="test1_data_data"
|
|
866
|
+
params={{ page: pagination_pg_0_page, perPage: 3 }}
|
|
867
|
+
fetchData={(params) =>
|
|
868
|
+
fetch(`/api/javascript-data-34a5011c?${new URLSearchParams(params).toString()}`)
|
|
869
|
+
.then((res) => res.json())
|
|
870
|
+
.then((data) => data.data)
|
|
871
|
+
}
|
|
872
|
+
persistDataDuringLoading={true}
|
|
873
|
+
key={`test1_data_data-page-${pagination_pg_0_page}`}
|
|
874
|
+
>
|
|
875
|
+
{/* Array mapper content */}
|
|
876
|
+
</DataProvider>
|
|
877
|
+
|
|
878
|
+
{/* Pagination buttons same as page example */}
|
|
879
|
+
</div>
|
|
880
|
+
)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
export default MyComponent
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
## Edge Cases & Special Handling
|
|
889
|
+
|
|
890
|
+
### 1. Same Data Source, Different perPage Values
|
|
891
|
+
|
|
892
|
+
**Scenario**: `test_users_data` used twice with `perPage: 10` and `perPage: 5`
|
|
893
|
+
|
|
894
|
+
**Handling**:
|
|
895
|
+
- Key `perPageMap` by `renderPropIdentifier` (not data source identifier)
|
|
896
|
+
- Generate separate `fetchData` calls in `getStaticProps`
|
|
897
|
+
- Each pagination has unique state variables
|
|
898
|
+
- Count fetched only once, used for both maxPages calculations
|
|
899
|
+
|
|
900
|
+
### 2. Mixed Paginated and Non-Paginated
|
|
901
|
+
|
|
902
|
+
**Scenario**: `test_users_data` used both paginated and non-paginated on same page
|
|
903
|
+
|
|
904
|
+
**Handling**:
|
|
905
|
+
- Non-paginated: `fetchData()` with no params, full data
|
|
906
|
+
- Paginated: `fetchData({ page: 1, perPage: X })`, subset of data
|
|
907
|
+
- Each DataProvider gets appropriate `initialData` reference
|
|
908
|
+
|
|
909
|
+
### 3. No perPage Specified
|
|
910
|
+
|
|
911
|
+
**Handling**: Default to `perPage: 10`
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
const perPage = perPageMap.get(lookupKey) || 10
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
### 4. Empty Data Sources
|
|
918
|
+
|
|
919
|
+
**Handling**:
|
|
920
|
+
- Count returns 0
|
|
921
|
+
- maxPages becomes 0
|
|
922
|
+
- Next button disabled immediately
|
|
923
|
+
- Previous button disabled (page starts at 1)
|
|
924
|
+
|
|
925
|
+
### 5. Static Data Sources (No Database)
|
|
926
|
+
|
|
927
|
+
**Scenario**: JavaScript array, CSV file, static collection
|
|
928
|
+
|
|
929
|
+
**Handling**:
|
|
930
|
+
- Still need API routes for client-side fetching
|
|
931
|
+
- Pagination applied in handler:
|
|
932
|
+
```javascript
|
|
933
|
+
// In handler
|
|
934
|
+
if (Array.isArray(data)) {
|
|
935
|
+
const limitValue = limit || perPage
|
|
936
|
+
const offsetValue = offset !== undefined ? parseInt(offset) :
|
|
937
|
+
(page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : 0)
|
|
938
|
+
|
|
939
|
+
if (limitValue) {
|
|
940
|
+
data = data.slice(offsetValue, offsetValue + parseInt(limitValue))
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### 6. Filters and Queries with Pagination
|
|
946
|
+
|
|
947
|
+
**Important**: `getCount` must apply the same filters as `handler`
|
|
948
|
+
|
|
949
|
+
```typescript
|
|
950
|
+
export async function getCount(req, res) {
|
|
951
|
+
const { query, queryColumns, filters } = req.query
|
|
952
|
+
|
|
953
|
+
// Apply same filters as data handler
|
|
954
|
+
if (queryColumns && query) {
|
|
955
|
+
const columns = Array.isArray(queryColumns) ? queryColumns : [queryColumns]
|
|
956
|
+
const searchConditions = columns.map(col => `${col} ILIKE $?`).join(' OR ')
|
|
957
|
+
conditions.push(`(${searchConditions})`)
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
if (filters) {
|
|
961
|
+
const parsedFilters = JSON.parse(filters)
|
|
962
|
+
for (const filter of parsedFilters) {
|
|
963
|
+
conditions.push(`${filter.column} ${filter.operator} $?`)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Execute COUNT with same conditions
|
|
968
|
+
}
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### 7. Nested or Conditional Array Mappers
|
|
972
|
+
|
|
973
|
+
**Handling**: The `extractPerPageValuesEarly` function traverses all UIDL structures:
|
|
974
|
+
- Conditional nodes
|
|
975
|
+
- Nested data sources
|
|
976
|
+
- Success/error/loading states
|
|
977
|
+
- List/empty states
|
|
978
|
+
|
|
979
|
+
Must handle all these structures or `perPage` will be missed.
|
|
980
|
+
|
|
981
|
+
---
|
|
982
|
+
|
|
983
|
+
## Testing & Verification
|
|
984
|
+
|
|
985
|
+
### Console Logs for Debugging (Now Removed)
|
|
986
|
+
|
|
987
|
+
During development, these logs were crucial:
|
|
988
|
+
|
|
989
|
+
```typescript
|
|
990
|
+
// Early extraction
|
|
991
|
+
console.log('[EARLY EXTRACT] cms-list-repeater', { perPage, renderProp })
|
|
992
|
+
console.log('[EARLY EXTRACT] perPage map:', Array.from(perPageMap.entries()))
|
|
993
|
+
|
|
994
|
+
// Pagination detection
|
|
995
|
+
console.log('[PAGINATION] Found DataProvider with name:', identifier)
|
|
996
|
+
console.log('[PAGINATION] Found pagination container:', className)
|
|
997
|
+
console.log('[PAGINATION] Array mapper renderProps:', arrayMapperRenderProps)
|
|
998
|
+
|
|
999
|
+
// getStaticProps modification
|
|
1000
|
+
console.log('[PAGINATION] Mapped import to variable:', importToDataSource)
|
|
1001
|
+
console.log('[PAGINATION] Promise.all now has X elements')
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
**Why They Were Important**:
|
|
1005
|
+
- UIDL transformations are not visible in the code
|
|
1006
|
+
- JSX AST structure is complex
|
|
1007
|
+
- Need to verify data flow through multiple plugin stages
|
|
1008
|
+
- Debugging timing issues (what runs when)
|
|
1009
|
+
|
|
1010
|
+
**Now Removed**: Production code should be clean, but this section documents what to add back if debugging.
|
|
1011
|
+
|
|
1012
|
+
### Manual Testing Checklist
|
|
1013
|
+
|
|
1014
|
+
- [ ] Single pagination on page
|
|
1015
|
+
- [ ] Multiple paginations on same page
|
|
1016
|
+
- [ ] Same data source, different perPage
|
|
1017
|
+
- [ ] Same data source, mixed paginated/non-paginated
|
|
1018
|
+
- [ ] Page navigation (prev/next buttons work)
|
|
1019
|
+
- [ ] Button disabled states (first page, last page)
|
|
1020
|
+
- [ ] Initial data loads correctly (page 1)
|
|
1021
|
+
- [ ] Subsequent pages fetch correctly
|
|
1022
|
+
- [ ] Count is accurate
|
|
1023
|
+
- [ ] maxPages calculated correctly
|
|
1024
|
+
- [ ] Component pagination (useEffect count fetch)
|
|
1025
|
+
- [ ] Page pagination (getStaticProps count fetch)
|
|
1026
|
+
- [ ] All data source types (JS, PostgreSQL, MySQL, MongoDB, etc.)
|
|
1027
|
+
- [ ] With filters/queries applied
|
|
1028
|
+
|
|
1029
|
+
### Known Limitations
|
|
1030
|
+
|
|
1031
|
+
1. **Page number validation**: No validation that requested page exists (page 999 of 10 will return empty)
|
|
1032
|
+
2. **Race conditions**: Rapid clicking may cause multiple fetches
|
|
1033
|
+
3. **Error states**: No specific UI for fetch errors
|
|
1034
|
+
4. **Loading states**: Uses DataProvider's built-in loading, no custom pagination loading
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
## Future Considerations
|
|
1039
|
+
|
|
1040
|
+
### Potential Enhancements
|
|
1041
|
+
|
|
1042
|
+
1. **Page number display**: Show "Page 1 of 10"
|
|
1043
|
+
2. **Jump to page**: Input to go directly to a page
|
|
1044
|
+
3. **Page size selector**: Allow user to change perPage
|
|
1045
|
+
4. **First/Last buttons**: Jump to first/last page
|
|
1046
|
+
5. **URL sync option**: Optional URL-based state for deep linking
|
|
1047
|
+
6. **Cursor-based pagination**: Support cursor/token-based pagination for large datasets
|
|
1048
|
+
7. **Virtual scrolling**: Infinite scroll with pagination
|
|
1049
|
+
8. **Optimistic updates**: Show loading state during page changes
|
|
1050
|
+
|
|
1051
|
+
### Potential Issues to Watch
|
|
1052
|
+
|
|
1053
|
+
1. **Memory leaks**: Ensure useEffect cleanup in components
|
|
1054
|
+
2. **Stale closures**: Be careful with state in callbacks
|
|
1055
|
+
3. **SSR hydration**: Ensure client/server state matches
|
|
1056
|
+
4. **Cache invalidation**: When to refetch count
|
|
1057
|
+
5. **Concurrent mode**: React 18+ concurrent features
|
|
1058
|
+
|
|
1059
|
+
### Breaking Changes to Avoid
|
|
1060
|
+
|
|
1061
|
+
1. Don't change UIDL structure without updating validator
|
|
1062
|
+
2. Don't remove `renderPropIdentifier` from cms-list-repeater
|
|
1063
|
+
3. Don't change perPageMap key structure
|
|
1064
|
+
4. Don't modify DataProvider API expectations
|
|
1065
|
+
5. Don't change getStaticProps return shape
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## File Reference
|
|
1070
|
+
|
|
1071
|
+
### Core Implementation Files
|
|
1072
|
+
|
|
1073
|
+
- `src/index.ts`: Early perPage extraction, plugin orchestration
|
|
1074
|
+
- `src/pagination-plugin.ts`: Main pagination plugin, JSX traversal, code generation
|
|
1075
|
+
- `src/array-mapper-pagination.ts`: Pagination logic helper, types
|
|
1076
|
+
- `src/utils.ts`: API route extraction
|
|
1077
|
+
- `src/data-source-fetchers.ts`: fetchData and fetchCount generation
|
|
1078
|
+
- `src/count-fetchers.ts`: getCount handler generation for all data source types
|
|
1079
|
+
- `src/fetchers/*.ts`: Individual data source handlers
|
|
1080
|
+
|
|
1081
|
+
### Type Definitions
|
|
1082
|
+
|
|
1083
|
+
- `packages/teleport-types/src/uidl.ts`: UIDL types, added `paginated` and `perPage`
|
|
1084
|
+
- `packages/teleport-uidl-validator/src/decoders/utils.ts`: UIDL validator, added pagination fields
|
|
1085
|
+
|
|
1086
|
+
### Tests
|
|
1087
|
+
|
|
1088
|
+
- `__tests__/pagination.test.ts`: Unit tests for pagination logic generation
|
|
1089
|
+
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
## Key Takeaways for Future Developers
|
|
1093
|
+
|
|
1094
|
+
1. **UIDL transformations happen early**: You cannot rely on UIDL node types in late-stage plugins. Use JSX AST traversal with class name detection.
|
|
1095
|
+
|
|
1096
|
+
2. **perPage must be extracted early**: Before any transformations, in the main plugin entry point.
|
|
1097
|
+
|
|
1098
|
+
3. **renderPropIdentifier is the unique key**: Not the data source identifier. Each array mapper instance has a unique renderPropIdentifier.
|
|
1099
|
+
|
|
1100
|
+
4. **DataProvider behavior is complex**: The `initialData` + `passFetchBecauseWeHaveInitialData` ref interaction requires conditional initialData and key prop to force remounting.
|
|
1101
|
+
|
|
1102
|
+
5. **Multiple instances need separate fetches**: Same data source used multiple times = multiple fetchData calls in getStaticProps, not one shared call.
|
|
1103
|
+
|
|
1104
|
+
6. **Count and data are separate**: Never couple them in the same response. Separate handlers allow independent fetching and optimization.
|
|
1105
|
+
|
|
1106
|
+
7. **Deduplication is critical**: Count fetches and useEffect calls must be deduplicated by data source to avoid unnecessary API calls.
|
|
1107
|
+
|
|
1108
|
+
8. **Variable naming must be unique**: When same data source used multiple times, every variable (data, count, maxPages) needs unique naming per instance.
|
|
1109
|
+
|
|
1110
|
+
9. **API routes must exist**: Even for static sources, client-side pagination requires API routes for fetchData.
|
|
1111
|
+
|
|
1112
|
+
10. **Console logs are debugging lifesavers**: The transformation pipeline is opaque. Strategic logging is essential during development, but remove for production.
|
|
1113
|
+
|
|
1114
|
+
---
|
|
1115
|
+
|
|
1116
|
+
## Conclusion
|
|
1117
|
+
|
|
1118
|
+
This pagination implementation handles complex edge cases and multiple architectural constraints. The key challenges were:
|
|
1119
|
+
|
|
1120
|
+
1. Working with transformed UIDL (JSX AST traversal)
|
|
1121
|
+
2. Early extraction of configuration before transformations
|
|
1122
|
+
3. Understanding and working around DataProvider's internal behavior
|
|
1123
|
+
4. Handling multiple instances of same data source
|
|
1124
|
+
5. Proper deduplication of fetches and effects
|
|
1125
|
+
6. Correct variable naming and scope management
|
|
1126
|
+
|
|
1127
|
+
The implementation is production-ready and handles all known edge cases. Future enhancements should maintain backward compatibility with the UIDL structure and generated code expectations.
|
|
1128
|
+
|