@teleporthq/teleport-plugin-next-data-source 0.43.69 → 0.43.71

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 (58) hide show
  1. package/__tests__/rest-list-edge-cases.test.ts +211 -0
  2. package/__tests__/shared-resource-prefetch.test.ts +111 -0
  3. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  4. package/dist/cjs/fetchers/rest-api.js +25 -1
  5. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  6. package/dist/cjs/index.d.ts.map +1 -1
  7. package/dist/cjs/index.js +3 -2
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/infinite-scroll.d.ts +7 -1
  10. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  11. package/dist/cjs/infinite-scroll.js +57 -8
  12. package/dist/cjs/infinite-scroll.js.map +1 -1
  13. package/dist/cjs/items-path.d.ts.map +1 -1
  14. package/dist/cjs/items-path.js +4 -3
  15. package/dist/cjs/items-path.js.map +1 -1
  16. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  17. package/dist/cjs/pagination-plugin.js +5 -2
  18. package/dist/cjs/pagination-plugin.js.map +1 -1
  19. package/dist/cjs/repeater-page-size.d.ts +16 -0
  20. package/dist/cjs/repeater-page-size.d.ts.map +1 -0
  21. package/dist/cjs/repeater-page-size.js +27 -0
  22. package/dist/cjs/repeater-page-size.js.map +1 -0
  23. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  24. package/dist/cjs/utils.d.ts.map +1 -1
  25. package/dist/cjs/utils.js +31 -4
  26. package/dist/cjs/utils.js.map +1 -1
  27. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  28. package/dist/esm/fetchers/rest-api.js +25 -1
  29. package/dist/esm/fetchers/rest-api.js.map +1 -1
  30. package/dist/esm/index.d.ts.map +1 -1
  31. package/dist/esm/index.js +3 -2
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/infinite-scroll.d.ts +7 -1
  34. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  35. package/dist/esm/infinite-scroll.js +57 -8
  36. package/dist/esm/infinite-scroll.js.map +1 -1
  37. package/dist/esm/items-path.d.ts.map +1 -1
  38. package/dist/esm/items-path.js +4 -3
  39. package/dist/esm/items-path.js.map +1 -1
  40. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  41. package/dist/esm/pagination-plugin.js +5 -2
  42. package/dist/esm/pagination-plugin.js.map +1 -1
  43. package/dist/esm/repeater-page-size.d.ts +16 -0
  44. package/dist/esm/repeater-page-size.d.ts.map +1 -0
  45. package/dist/esm/repeater-page-size.js +23 -0
  46. package/dist/esm/repeater-page-size.js.map +1 -0
  47. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  48. package/dist/esm/utils.d.ts.map +1 -1
  49. package/dist/esm/utils.js +31 -4
  50. package/dist/esm/utils.js.map +1 -1
  51. package/package.json +5 -5
  52. package/src/fetchers/rest-api.ts +27 -1
  53. package/src/index.ts +3 -2
  54. package/src/infinite-scroll.ts +107 -43
  55. package/src/items-path.ts +3 -2
  56. package/src/pagination-plugin.ts +6 -2
  57. package/src/repeater-page-size.ts +24 -0
  58. package/src/utils.ts +54 -6
@@ -78,12 +78,38 @@ interface RESTAPIConfig {
78
78
  url?: string
79
79
  method?: string
80
80
  headers?: Record<string, string>
81
+ queryParams?: Record<string, string>
81
82
  authorization?: Authorization
82
83
  bodyType?: string
83
84
  body?: string
84
85
  cache?: RESTAPICacheConfig
85
86
  }
86
87
 
88
+ /**
89
+ * The URL with the data source's query params appended, built the way the
90
+ * editor's own fetch builds it (`RestApiService.constructUrl` in the
91
+ * data-fetcher worker: `searchParams.append` per param).
92
+ *
93
+ * The params used to be dropped here, so the published site asked the API a
94
+ * different question than the editor did — `?per_page=10` or `?category=x`
95
+ * configured on the source, and every row of the unfiltered response came back.
96
+ */
97
+ const buildRequestURL = (
98
+ url: string | undefined,
99
+ queryParams?: Record<string, string>
100
+ ): string | undefined => {
101
+ const params = Object.entries(queryParams || {}).filter(
102
+ ([key, value]) => key.trim() !== '' && typeof value === 'string'
103
+ )
104
+ if (!url || params.length === 0) {
105
+ return url
106
+ }
107
+
108
+ const requestURL = new URL(url)
109
+ params.forEach(([key, value]) => requestURL.searchParams.append(key, value))
110
+ return requestURL.toString()
111
+ }
112
+
87
113
  export const generateRESTAPIFetcher = (config: Record<string, unknown>): string => {
88
114
  const restConfig = config as RESTAPIConfig
89
115
  const authCode = generateAuthCode(restConfig.authorization || {})
@@ -110,7 +136,7 @@ export default async function handler(req, res) {
110
136
  try {
111
137
  const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset, collectionPath, itemsPath } = req.query
112
138
 
113
- const url = ${JSON.stringify(restConfig.url)}
139
+ const url = ${JSON.stringify(buildRequestURL(restConfig.url, restConfig.queryParams))}
114
140
  const method = ${JSON.stringify(restConfig.method || 'GET')}
115
141
 
116
142
  const headers = ${JSON.stringify(restConfig.headers || {})}
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  hasUnresolvableDynamicParams,
16
16
  } from './utils'
17
17
  import { createNextArrayMapperPaginationPlugin } from './pagination-plugin'
18
+ import { resolveRepeaterPerPage } from './repeater-page-size'
18
19
  import { buildProductTransformOptions } from './transformations'
19
20
  import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
20
21
  import { ensureRouterDeclaration } from './request-locale'
@@ -416,8 +417,8 @@ function extractPaginationConfigEarly(uidlNode: any, resources: any): Pagination
416
417
  const searchEnabled = node.content?.searchEnabled
417
418
  const searchDebounce = node.content?.searchDebounce
418
419
 
419
- if (paginated && perPage && renderProp) {
420
- perPageMap.set(renderProp, perPage)
420
+ if (paginated && renderProp) {
421
+ perPageMap.set(renderProp, resolveRepeaterPerPage(perPage))
421
422
  }
422
423
 
423
424
  if (searchEnabled && renderProp) {
@@ -109,20 +109,124 @@ export const buildInfiniteScrollDeclarations = (
109
109
  return declarations
110
110
  }
111
111
 
112
+ const isIndexSegment = (segment: string): boolean => /^\d+$/.test(segment)
113
+
114
+ const readSegment = (object: types.Expression, segment: string): types.OptionalMemberExpression =>
115
+ isIndexSegment(segment)
116
+ ? types.optionalMemberExpression(object, types.numericLiteral(Number(segment)), true, true)
117
+ : types.isValidIdentifier(segment)
118
+ ? types.optionalMemberExpression(object, types.identifier(segment), false, true)
119
+ : types.optionalMemberExpression(object, types.stringLiteral(segment), true, true)
120
+
121
+ /** `payload?.data?.items` for `['data', 'items']`. */
122
+ const readAtPath = (payload: types.Expression, path: string[]): types.Expression =>
123
+ path.reduce<types.Expression>((object, segment) => readSegment(object, segment), payload)
124
+
125
+ /**
126
+ * A copy of `node` with `value` at `path`, every level around it kept:
127
+ * `{ ...payload, data: { ...payload?.data, items: value } }`. An index segment
128
+ * copies the list it indexes instead of spreading it into an object.
129
+ */
130
+ const writeAtPath = (
131
+ node: types.Expression,
132
+ path: string[],
133
+ value: types.Expression
134
+ ): types.Expression => {
135
+ if (path.length === 0) {
136
+ return value
137
+ }
138
+ const [segment, ...rest] = path
139
+ const child = writeAtPath(readSegment(types.cloneNode(node, true), segment), rest, value)
140
+ if (isIndexSegment(segment)) {
141
+ return types.callExpression(
142
+ types.memberExpression(types.identifier('Object'), types.identifier('assign')),
143
+ [
144
+ types.arrayExpression([
145
+ types.spreadElement(
146
+ types.logicalExpression('||', types.cloneNode(node, true), types.arrayExpression([]))
147
+ ),
148
+ ]),
149
+ types.objectExpression([
150
+ types.objectProperty(types.numericLiteral(Number(segment)), child),
151
+ ]),
152
+ ]
153
+ )
154
+ }
155
+ return types.objectExpression([
156
+ types.spreadElement(types.cloneNode(node, true)),
157
+ types.objectProperty(
158
+ types.isValidIdentifier(segment) ? types.identifier(segment) : types.stringLiteral(segment),
159
+ child
160
+ ),
161
+ ])
162
+ }
163
+
112
164
  /**
113
165
  * The `.then(...)` tail that accumulates a page and reports whether more exist.
114
166
  *
115
167
  * Returned as the resolved value of `fetchData`, so `DataProvider` stores the
116
168
  * whole accumulated list as its data.
169
+ *
170
+ * `itemsPath` is where the rows sit inside a payload that wraps them
171
+ * (`{ items: [...], total }`): the rows are read there and the accumulated list
172
+ * is put back there, so the page keeps reading it through the same path. The
173
+ * wrapper itself counted as one "row" before, which ended the list after its
174
+ * first page and rendered nothing.
117
175
  */
118
176
  export const buildAccumulatingResponseHandler = (
119
177
  vars: InfiniteScrollVars,
120
- perPage: number
178
+ perPage: number,
179
+ itemsPath?: string[]
121
180
  ): types.ArrowFunctionExpression => {
122
181
  const accumCurrent = (): types.MemberExpression =>
123
182
  types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
124
183
  const accumPages = (): types.MemberExpression =>
125
184
  types.memberExpression(accumCurrent(), types.identifier('pages'))
185
+ const payload = (): types.OptionalMemberExpression =>
186
+ types.optionalMemberExpression(
187
+ types.identifier('response'),
188
+ types.identifier('data'),
189
+ false,
190
+ true
191
+ )
192
+ const rowsPath = itemsPath?.length ? itemsPath : undefined
193
+
194
+ const accumulatedRows = types.callExpression(
195
+ types.memberExpression(
196
+ types.callExpression(
197
+ types.memberExpression(
198
+ types.callExpression(
199
+ types.memberExpression(
200
+ types.callExpression(
201
+ types.memberExpression(types.identifier('Object'), types.identifier('keys')),
202
+ [accumPages()]
203
+ ),
204
+ types.identifier('map')
205
+ ),
206
+ [types.identifier('Number')]
207
+ ),
208
+ types.identifier('sort')
209
+ ),
210
+ [
211
+ types.arrowFunctionExpression(
212
+ [types.identifier('a'), types.identifier('b')],
213
+ types.binaryExpression('-', types.identifier('a'), types.identifier('b'))
214
+ ),
215
+ ]
216
+ ),
217
+ types.identifier('reduce')
218
+ ),
219
+ [
220
+ types.arrowFunctionExpression(
221
+ [types.identifier('acc'), types.identifier('p')],
222
+ types.callExpression(
223
+ types.memberExpression(types.identifier('acc'), types.identifier('concat')),
224
+ [types.memberExpression(accumPages(), types.identifier('p'), true)]
225
+ )
226
+ ),
227
+ types.arrayExpression([]),
228
+ ]
229
+ )
126
230
 
127
231
  return types.arrowFunctionExpression(
128
232
  [types.identifier('response')],
@@ -132,12 +236,7 @@ export const buildAccumulatingResponseHandler = (
132
236
  types.identifier('rows'),
133
237
  types.logicalExpression(
134
238
  '||',
135
- types.optionalMemberExpression(
136
- types.identifier('response'),
137
- types.identifier('data'),
138
- false,
139
- true
140
- ),
239
+ rowsPath ? readAtPath(payload(), rowsPath) : payload(),
141
240
  types.arrayExpression([])
142
241
  )
143
242
  ),
@@ -211,42 +310,7 @@ export const buildAccumulatingResponseHandler = (
211
310
  // page 10 does not land between 1 and 2. This is what DataProvider stores
212
311
  // as its data, and so what the list renders.
213
312
  types.returnStatement(
214
- types.callExpression(
215
- types.memberExpression(
216
- types.callExpression(
217
- types.memberExpression(
218
- types.callExpression(
219
- types.memberExpression(
220
- types.callExpression(
221
- types.memberExpression(types.identifier('Object'), types.identifier('keys')),
222
- [accumPages()]
223
- ),
224
- types.identifier('map')
225
- ),
226
- [types.identifier('Number')]
227
- ),
228
- types.identifier('sort')
229
- ),
230
- [
231
- types.arrowFunctionExpression(
232
- [types.identifier('a'), types.identifier('b')],
233
- types.binaryExpression('-', types.identifier('a'), types.identifier('b'))
234
- ),
235
- ]
236
- ),
237
- types.identifier('reduce')
238
- ),
239
- [
240
- types.arrowFunctionExpression(
241
- [types.identifier('acc'), types.identifier('p')],
242
- types.callExpression(
243
- types.memberExpression(types.identifier('acc'), types.identifier('concat')),
244
- [types.memberExpression(accumPages(), types.identifier('p'), true)]
245
- )
246
- ),
247
- types.arrayExpression([]),
248
- ]
249
- )
313
+ rowsPath ? writeAtPath(payload(), rowsPath, accumulatedRows) : accumulatedRows
250
314
  ),
251
315
  ])
252
316
  )
package/src/items-path.ts CHANGED
@@ -16,7 +16,7 @@ import * as types from '@babel/types'
16
16
  * render prop — an expression this cannot read is left exactly as before.
17
17
  */
18
18
  const SEGMENT_RE =
19
- /^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)')\s*\])/
19
+ /^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)'|(\d+))\s*\])/
20
20
 
21
21
  const PATH_AWARE_SOURCE_TYPES = new Set(['rest-api', 'javascript'])
22
22
 
@@ -47,7 +47,8 @@ export const extractItemsPath = (
47
47
  if (!match) {
48
48
  return undefined
49
49
  }
50
- path.push(match[1] ?? match[2] ?? match[3] ?? match[4])
50
+ // An index (`?.[0]`) is kept as its digits; the handler reads it off a list.
51
+ path.push(match[1] ?? match[2] ?? match[3] ?? match[4] ?? match[5])
51
52
  rest = rest.slice(match[0].length)
52
53
  }
53
54
 
@@ -14,6 +14,7 @@ import { buildProductTransformOptions, type EntityTransformOptions } from './tra
14
14
  import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
15
15
  import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './filter-utils'
16
16
  import { appendItemsPathParam, extractItemsPath } from './items-path'
17
+ import { resolveRepeaterPerPage } from './repeater-page-size'
17
18
  import {
18
19
  applyLoadingStateToDataProvider,
19
20
  buildLoadingStateDeclarations,
@@ -606,7 +607,9 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
606
607
 
607
608
  // For paginated mappers, use perPage from cms-list-repeater
608
609
  // For plain mappers, use limit from data-source-list resource params
609
- const effectivePerPage = content.paginated ? content.perPage : limit || content.perPage
610
+ const effectivePerPage = content.paginated
611
+ ? resolveRepeaterPerPage(content.perPage)
612
+ : limit || content.perPage
610
613
 
611
614
  const searchDefaultValue = parseSearchDefaultValue(content.searchDefaultValue)
612
615
 
@@ -2952,7 +2955,8 @@ function createFetchDataAttribute(
2952
2955
  const responseTail = accumulate
2953
2956
  ? buildAccumulatingResponseHandler(
2954
2957
  getInfiniteScrollVars(accumulate.usage.index),
2955
- accumulate.usage.perPage
2958
+ accumulate.usage.perPage,
2959
+ accumulate.usage.itemsPath
2956
2960
  )
2957
2961
  : cache
2958
2962
  ? buildCacheStoreThen(cache)
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Rows per page a paginated list gets when its UIDL names none: the editor's
3
+ * own default (`DEFAULT_ARRAY_MAPPER_PER_PAGE`), which is also what the canvas
4
+ * pages by.
5
+ */
6
+ export const DEFAULT_REPEATER_PER_PAGE = 10
7
+
8
+ /**
9
+ * The page size of a paginated `cms-list-repeater`: its `perPage` when that is
10
+ * a whole positive number, else the editor's default.
11
+ *
12
+ * ⛔ A paginated list without a usable page size used to take the whole project
13
+ * down with it ("Property value expected type of number but got undefined"),
14
+ * and a NaN one paged nothing — while the canvas showed ten rows per page.
15
+ */
16
+ export const resolveRepeaterPerPage = (perPage: unknown): number => {
17
+ const parsed =
18
+ typeof perPage === 'number'
19
+ ? perPage
20
+ : typeof perPage === 'string'
21
+ ? parseInt(perPage, 10)
22
+ : NaN
23
+ return Number.isFinite(parsed) && parsed >= 1 ? Math.floor(parsed) : DEFAULT_REPEATER_PER_PAGE
24
+ }
package/src/utils.ts CHANGED
@@ -644,7 +644,15 @@ export const generateCollectionPathHelperCode = (): string => {
644
644
  const readPayloadPath = (payload, path) => {
645
645
  let current = payload
646
646
  for (const segment of path) {
647
- if (!current || typeof current !== 'object' || Array.isArray(current) || !(segment in current)) {
647
+ if (Array.isArray(current)) {
648
+ const index = Number(segment)
649
+ if (!Number.isInteger(index) || index < 0 || index >= current.length) {
650
+ return undefined
651
+ }
652
+ current = current[index]
653
+ continue
654
+ }
655
+ if (!current || typeof current !== 'object' || !(segment in current)) {
648
656
  return undefined
649
657
  }
650
658
  current = current[segment]
@@ -668,8 +676,16 @@ const openItemsEnvelope = (payload, rawPath) => {
668
676
  if (!path) return null
669
677
  const items = readPayloadPath(payload, path)
670
678
  if (!Array.isArray(items)) return null
671
- const rebuild = (node, depth, next) =>
672
- depth === path.length ? next : { ...node, [path[depth]]: rebuild(node[path[depth]], depth + 1, next) }
679
+ const rebuild = (node, depth, next) => {
680
+ if (depth === path.length) return next
681
+ const child = rebuild(node[path[depth]], depth + 1, next)
682
+ if (Array.isArray(node)) {
683
+ const copy = node.slice()
684
+ copy[Number(path[depth])] = child
685
+ return copy
686
+ }
687
+ return { ...node, [path[depth]]: child }
688
+ }
673
689
  return { items, close: (next) => rebuild(payload, 0, next) }
674
690
  }`
675
691
  }
@@ -1217,17 +1233,20 @@ export const extractDataSourceIntoGetStaticProps = (
1217
1233
  )
1218
1234
  const baseKey = `${sanitizedDsName}_${sanitizedTableName}_data`
1219
1235
 
1220
- let propKey: string
1236
+ let resourcePropKey: string
1221
1237
  if (renderPropIdentifier && resourceId) {
1222
1238
  // Include resource ID to differentiate between same renderProp but different params
1223
1239
  const sanitizedResourceId = StringUtils.dashCaseToCamelCase(
1224
1240
  sanitizeFileName(resourceId).replace(/^TQ_/, '')
1225
1241
  )
1226
- propKey = `${renderPropIdentifier}_${sanitizedResourceId}`
1242
+ resourcePropKey = `${renderPropIdentifier}_${sanitizedResourceId}`
1227
1243
  } else {
1228
1244
  // Use base key for uniqueness (even if renderPropIdentifier exists, base key ensures uniqueness across different data sources)
1229
- propKey = baseKey
1245
+ resourcePropKey = baseKey
1230
1246
  }
1247
+ // tslint:disable-next-line:no-any
1248
+ const nodeResource = (node.content as any).resource
1249
+ const propKey = claimParamsPropKey(componentChunk, resourcePropKey, nodeResource)
1231
1250
 
1232
1251
  // Find matching JSX nodes for this data source
1233
1252
  // Strategy depends on whether we have a unique resource ID:
@@ -1821,6 +1840,35 @@ interface ParallelFetchMeta {
1821
1840
  declaration?: types.VariableDeclaration
1822
1841
  }
1823
1842
 
1843
+ /**
1844
+ * The getStaticProps prop a provider's prefetch lands in: `resourcePropKey`,
1845
+ * suffixed when a provider with DIFFERENT params already holds it on this page.
1846
+ *
1847
+ * Every provider bound to the same table carries the same resource id
1848
+ * (`TQ_<source><table>`) and its own params, so the resource-derived key alone
1849
+ * gave a list limited to 10 rows and a text bound to the same source ONE
1850
+ * prefetch — whichever ran first. The other rendered its rows (`initialData`
1851
+ * skips the provider's own first fetch): every row in the limited list, or ten
1852
+ * where the text read the fifteenth. Providers with equal params still share
1853
+ * one key, and one fetch.
1854
+ */
1855
+ const claimParamsPropKey = (
1856
+ componentChunk: ChunkDefinition,
1857
+ resourcePropKey: string,
1858
+ resource: { params?: Record<string, unknown> } | undefined
1859
+ ): string => {
1860
+ const meta = componentChunk.meta as { dataSourcePropKeyParams?: Record<string, string> }
1861
+ const claimed = meta.dataSourcePropKeyParams ?? (meta.dataSourcePropKeyParams = {})
1862
+ const signature = JSON.stringify(resource?.params ?? {})
1863
+
1864
+ let propKey = resourcePropKey
1865
+ for (let suffix = 2; claimed[propKey] !== undefined && claimed[propKey] !== signature; suffix++) {
1866
+ propKey = `${resourcePropKey}_${suffix}`
1867
+ }
1868
+ claimed[propKey] = signature
1869
+ return propKey
1870
+ }
1871
+
1824
1872
  const createSafeFetchExpression = (
1825
1873
  fetchCallExpression: types.CallExpression,
1826
1874
  label: string