@teleporthq/teleport-plugin-next-data-source 0.43.67 → 0.43.68

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 (202) hide show
  1. package/__tests__/array-overlap-sql.test.ts +53 -0
  2. package/__tests__/blog-post-page-enrichment.test.ts +214 -0
  3. package/__tests__/browser-row-policy.test.ts +503 -0
  4. package/__tests__/data-source-cache-wrapping.test.ts +18 -2
  5. package/__tests__/ecommerce-product-options.test.ts +206 -0
  6. package/__tests__/ecommerce-product-pages.test.ts +82 -0
  7. package/__tests__/ecommerce-product-pre-options.golden.json +505 -0
  8. package/__tests__/ecommerce-product-storefront-contract.test.ts +293 -0
  9. package/__tests__/ecommerce-product-subscriptions.test.ts +121 -0
  10. package/__tests__/fetchers.test.ts +2 -2
  11. package/__tests__/items-path.test.ts +150 -0
  12. package/__tests__/product-options-storefront.fixture.json +2510 -0
  13. package/__tests__/product-page-url-fixtures.json +25 -0
  14. package/__tests__/product-price-sort.ts +4 -2
  15. package/__tests__/raw-rows-mode.test.ts +111 -0
  16. package/__tests__/rest-api-response-cache.test.ts +434 -0
  17. package/__tests__/table-read-route-protected-tables.test.ts +493 -0
  18. package/dist/cjs/array-overlap-sql.d.ts +21 -0
  19. package/dist/cjs/array-overlap-sql.d.ts.map +1 -0
  20. package/dist/cjs/array-overlap-sql.js +25 -0
  21. package/dist/cjs/array-overlap-sql.js.map +1 -0
  22. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  23. package/dist/cjs/data-source-fetchers.js +10 -4
  24. package/dist/cjs/data-source-fetchers.js.map +1 -1
  25. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/javascript.js +1 -1
  27. package/dist/cjs/fetchers/javascript.js.map +1 -1
  28. package/dist/cjs/fetchers/postgresql.d.ts +1 -1
  29. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/postgresql.js +8 -4
  31. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  32. package/dist/cjs/fetchers/raw-query.d.ts +8 -1
  33. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/raw-query.js +42 -4
  35. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  36. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/rest-api.js +7 -2
  38. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  39. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/teleport.js +8 -5
  41. package/dist/cjs/fetchers/teleport.js.map +1 -1
  42. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +43 -0
  43. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  44. package/dist/cjs/fetchers/utils/browser-row-policy.js +66 -0
  45. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -0
  46. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts +8 -0
  47. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  48. package/dist/cjs/fetchers/utils/request-sql-guards.js +12 -0
  49. package/dist/cjs/fetchers/utils/request-sql-guards.js.map +1 -0
  50. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  51. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  52. package/dist/cjs/fetchers/utils/rest-api-response-cache.js +64 -0
  53. package/dist/cjs/fetchers/utils/rest-api-response-cache.js.map +1 -0
  54. package/dist/cjs/fetchers/utils/table-access-guard.d.ts +23 -0
  55. package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -0
  56. package/dist/cjs/fetchers/utils/table-access-guard.js +34 -0
  57. package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -0
  58. package/dist/cjs/items-path.d.ts +10 -0
  59. package/dist/cjs/items-path.d.ts.map +1 -0
  60. package/dist/cjs/items-path.js +86 -0
  61. package/dist/cjs/items-path.js.map +1 -0
  62. package/dist/cjs/localized-columns.d.ts.map +1 -1
  63. package/dist/cjs/localized-columns.js +13 -0
  64. package/dist/cjs/localized-columns.js.map +1 -1
  65. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  66. package/dist/cjs/pagination-plugin.js +17 -5
  67. package/dist/cjs/pagination-plugin.js.map +1 -1
  68. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +16 -0
  69. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -0
  70. package/dist/cjs/transformations/blog-adjacent-posts.js +25 -0
  71. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -0
  72. package/dist/cjs/transformations/blog-comments.d.ts +30 -0
  73. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -0
  74. package/dist/cjs/transformations/blog-comments.js +34 -0
  75. package/dist/cjs/transformations/blog-comments.js.map +1 -0
  76. package/dist/cjs/transformations/blog-post.d.ts +2 -0
  77. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  78. package/dist/cjs/transformations/blog-post.js +4 -1
  79. package/dist/cjs/transformations/blog-post.js.map +1 -1
  80. package/dist/cjs/transformations/ecommerce-product.d.ts +9 -1
  81. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  82. package/dist/cjs/transformations/ecommerce-product.js +28 -1
  83. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  84. package/dist/cjs/transformations/heading-anchors.d.ts +25 -0
  85. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -0
  86. package/dist/cjs/transformations/heading-anchors.js +29 -0
  87. package/dist/cjs/transformations/heading-anchors.js.map +1 -0
  88. package/dist/cjs/transformations/index.d.ts +47 -4
  89. package/dist/cjs/transformations/index.d.ts.map +1 -1
  90. package/dist/cjs/transformations/index.js +56 -18
  91. package/dist/cjs/transformations/index.js.map +1 -1
  92. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  93. package/dist/cjs/transformations/shared-utils.js +1 -1
  94. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  95. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  96. package/dist/cjs/utils.d.ts.map +1 -1
  97. package/dist/cjs/utils.js +1 -1
  98. package/dist/cjs/utils.js.map +1 -1
  99. package/dist/esm/array-overlap-sql.d.ts +21 -0
  100. package/dist/esm/array-overlap-sql.d.ts.map +1 -0
  101. package/dist/esm/array-overlap-sql.js +21 -0
  102. package/dist/esm/array-overlap-sql.js.map +1 -0
  103. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  104. package/dist/esm/data-source-fetchers.js +11 -5
  105. package/dist/esm/data-source-fetchers.js.map +1 -1
  106. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  107. package/dist/esm/fetchers/javascript.js +1 -1
  108. package/dist/esm/fetchers/javascript.js.map +1 -1
  109. package/dist/esm/fetchers/postgresql.d.ts +1 -1
  110. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  111. package/dist/esm/fetchers/postgresql.js +8 -4
  112. package/dist/esm/fetchers/postgresql.js.map +1 -1
  113. package/dist/esm/fetchers/raw-query.d.ts +8 -1
  114. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  115. package/dist/esm/fetchers/raw-query.js +42 -4
  116. package/dist/esm/fetchers/raw-query.js.map +1 -1
  117. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  118. package/dist/esm/fetchers/rest-api.js +7 -2
  119. package/dist/esm/fetchers/rest-api.js.map +1 -1
  120. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  121. package/dist/esm/fetchers/teleport.js +8 -5
  122. package/dist/esm/fetchers/teleport.js.map +1 -1
  123. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +43 -0
  124. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  125. package/dist/esm/fetchers/utils/browser-row-policy.js +60 -0
  126. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -0
  127. package/dist/esm/fetchers/utils/request-sql-guards.d.ts +8 -0
  128. package/dist/esm/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  129. package/dist/esm/fetchers/utils/request-sql-guards.js +8 -0
  130. package/dist/esm/fetchers/utils/request-sql-guards.js.map +1 -0
  131. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  132. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  133. package/dist/esm/fetchers/utils/rest-api-response-cache.js +59 -0
  134. package/dist/esm/fetchers/utils/rest-api-response-cache.js.map +1 -0
  135. package/dist/esm/fetchers/utils/table-access-guard.d.ts +23 -0
  136. package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -0
  137. package/dist/esm/fetchers/utils/table-access-guard.js +29 -0
  138. package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -0
  139. package/dist/esm/items-path.d.ts +10 -0
  140. package/dist/esm/items-path.d.ts.map +1 -0
  141. package/dist/esm/items-path.js +58 -0
  142. package/dist/esm/items-path.js.map +1 -0
  143. package/dist/esm/localized-columns.d.ts.map +1 -1
  144. package/dist/esm/localized-columns.js +13 -0
  145. package/dist/esm/localized-columns.js.map +1 -1
  146. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  147. package/dist/esm/pagination-plugin.js +17 -5
  148. package/dist/esm/pagination-plugin.js.map +1 -1
  149. package/dist/esm/transformations/blog-adjacent-posts.d.ts +16 -0
  150. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -0
  151. package/dist/esm/transformations/blog-adjacent-posts.js +21 -0
  152. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -0
  153. package/dist/esm/transformations/blog-comments.d.ts +30 -0
  154. package/dist/esm/transformations/blog-comments.d.ts.map +1 -0
  155. package/dist/esm/transformations/blog-comments.js +30 -0
  156. package/dist/esm/transformations/blog-comments.js.map +1 -0
  157. package/dist/esm/transformations/blog-post.d.ts +2 -0
  158. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  159. package/dist/esm/transformations/blog-post.js +4 -1
  160. package/dist/esm/transformations/blog-post.js.map +1 -1
  161. package/dist/esm/transformations/ecommerce-product.d.ts +9 -1
  162. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  163. package/dist/esm/transformations/ecommerce-product.js +29 -2
  164. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  165. package/dist/esm/transformations/heading-anchors.d.ts +25 -0
  166. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -0
  167. package/dist/esm/transformations/heading-anchors.js +25 -0
  168. package/dist/esm/transformations/heading-anchors.js.map +1 -0
  169. package/dist/esm/transformations/index.d.ts +47 -4
  170. package/dist/esm/transformations/index.d.ts.map +1 -1
  171. package/dist/esm/transformations/index.js +56 -18
  172. package/dist/esm/transformations/index.js.map +1 -1
  173. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  174. package/dist/esm/transformations/shared-utils.js +1 -1
  175. package/dist/esm/transformations/shared-utils.js.map +1 -1
  176. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  177. package/dist/esm/utils.d.ts.map +1 -1
  178. package/dist/esm/utils.js +1 -1
  179. package/dist/esm/utils.js.map +1 -1
  180. package/package.json +5 -5
  181. package/src/array-overlap-sql.ts +41 -0
  182. package/src/data-source-fetchers.ts +41 -5
  183. package/src/fetchers/javascript.ts +9 -1
  184. package/src/fetchers/postgresql.ts +37 -10
  185. package/src/fetchers/raw-query.ts +60 -5
  186. package/src/fetchers/rest-api.ts +27 -5
  187. package/src/fetchers/teleport.ts +37 -13
  188. package/src/fetchers/utils/browser-row-policy.ts +212 -0
  189. package/src/fetchers/utils/request-sql-guards.ts +25 -0
  190. package/src/fetchers/utils/rest-api-response-cache.ts +188 -0
  191. package/src/fetchers/utils/table-access-guard.ts +39 -0
  192. package/src/items-path.ts +74 -0
  193. package/src/localized-columns.ts +13 -0
  194. package/src/pagination-plugin.ts +29 -7
  195. package/src/transformations/blog-adjacent-posts.ts +74 -0
  196. package/src/transformations/blog-comments.ts +132 -0
  197. package/src/transformations/blog-post.ts +38 -5
  198. package/src/transformations/ecommerce-product.ts +568 -5
  199. package/src/transformations/heading-anchors.ts +85 -0
  200. package/src/transformations/index.ts +165 -22
  201. package/src/transformations/shared-utils.ts +6 -0
  202. package/src/utils.ts +26 -4
@@ -0,0 +1,74 @@
1
+ import * as types from '@babel/types'
2
+
3
+ /**
4
+ * Where a paginated/searchable list's rows sit inside the payload its data
5
+ * source returns, read off the repeater's `source` expression.
6
+ *
7
+ * A REST API or JavaScript source may answer `{ templates: [...], total }`, and
8
+ * the editor binds the list to `All_Templates_data_data?.templates || []`. The
9
+ * generated fetcher only searches, filters, sorts and slices a payload that IS
10
+ * an array, so without this every row came back on every page (the canvas
11
+ * slices on the client and looked right) and the count measured the wrapper as
12
+ * 0 rows, disabling "Next".
13
+ *
14
+ * Returns undefined when the list is bound to the payload itself, or when the
15
+ * source is anything other than a plain property chain off the data source's
16
+ * render prop — an expression this cannot read is left exactly as before.
17
+ */
18
+ const SEGMENT_RE =
19
+ /^(?:\?\.([A-Za-z_$][\w$]*)|\.([A-Za-z_$][\w$]*)|(?:\?\.)?\[\s*(?:"([^"]*)"|'([^']*)')\s*\])/
20
+
21
+ const PATH_AWARE_SOURCE_TYPES = new Set(['rest-api', 'javascript'])
22
+
23
+ export const extractItemsPath = (
24
+ source: unknown,
25
+ dataSourceIdentifier: string,
26
+ dataSourceType: string
27
+ ): string[] | undefined => {
28
+ if (typeof source !== 'string' || !dataSourceIdentifier) {
29
+ return undefined
30
+ }
31
+ if (!PATH_AWARE_SOURCE_TYPES.has(dataSourceType)) {
32
+ return undefined
33
+ }
34
+
35
+ let rest = source
36
+ .trim()
37
+ .replace(/\s*\|\|\s*\[\s*\]$/, '')
38
+ .trim()
39
+ if (!rest.startsWith(dataSourceIdentifier)) {
40
+ return undefined
41
+ }
42
+ rest = rest.slice(dataSourceIdentifier.length)
43
+
44
+ const path: string[] = []
45
+ while (rest.length > 0) {
46
+ const match = rest.match(SEGMENT_RE)
47
+ if (!match) {
48
+ return undefined
49
+ }
50
+ path.push(match[1] ?? match[2] ?? match[3] ?? match[4])
51
+ rest = rest.slice(match[0].length)
52
+ }
53
+
54
+ return path.length > 0 ? path : undefined
55
+ }
56
+
57
+ /**
58
+ * Push `<key>: '["templates"]'` onto a params object. Fetch calls send it as
59
+ * `itemsPath` (process the inner array, keep the wrapper the page reads back
60
+ * through); count calls send it as `collectionPath`, so the count handler
61
+ * measures the inner array instead of the wrapper.
62
+ */
63
+ export const appendItemsPathParam = (
64
+ paramsProps: types.ObjectProperty[],
65
+ itemsPath: string[] | undefined,
66
+ key: 'itemsPath' | 'collectionPath'
67
+ ): void => {
68
+ if (!itemsPath || itemsPath.length === 0) {
69
+ return
70
+ }
71
+ paramsProps.push(
72
+ types.objectProperty(types.identifier(key), types.stringLiteral(JSON.stringify(itemsPath)))
73
+ )
74
+ }
@@ -118,6 +118,19 @@ export const generateLocalizedColumnsHelper = (families: LocalizedColumnMap): st
118
118
  " return '(' + clauses.join(' OR ') + ')'",
119
119
  '}',
120
120
  '',
121
+ '// A translatable list (tags) and its per-language copies, for a filter that',
122
+ '// matches a value in ANY language (a localized tag link resolves under every',
123
+ '// locale, like a localized slug). Null for a column with no copies.',
124
+ 'function localizedColumnList(field) {',
125
+ ' var copies = LOCALIZED_COLUMNS[field]',
126
+ ' if (!copies) return null',
127
+ ' var columns = [quoteLocalizedColumn(field)]',
128
+ ' for (var locale in copies) {',
129
+ ' if (Object.prototype.hasOwnProperty.call(copies, locale)) columns.push(quoteLocalizedColumn(copies[locale]))',
130
+ ' }',
131
+ ' return columns',
132
+ '}',
133
+ '',
121
134
  "// ORDER BY a translatable column in the request's language, falling back to",
122
135
  '// the base column for a row with no translation — the same resolution the',
123
136
  '// row transform applies to what is displayed. Null when the field or the',
@@ -13,6 +13,7 @@ import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
13
13
  import { buildProductTransformOptions, type EntityTransformOptions } from './transformations'
14
14
  import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
15
15
  import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './filter-utils'
16
+ import { appendItemsPathParam, extractItemsPath } from './items-path'
16
17
  import {
17
18
  applyLoadingStateToDataProvider,
18
19
  buildLoadingStateDeclarations,
@@ -255,6 +256,9 @@ interface DataSourceUsage {
255
256
  // `URLSearchParamSync` builders. Page 1 is written as the ABSENCE of the key.
256
257
  // Cleared when `infiniteScroll` is set.
257
258
  pageUrlParamKey?: string
259
+ // Path to the rows inside a wrapped REST/JavaScript payload, when the list is
260
+ // bound to one (`data?.templates` -> `['templates']`). See `items-path.ts`.
261
+ itemsPath?: string[]
258
262
  // Computed category
259
263
  category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
260
264
  }
@@ -656,6 +660,11 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
656
660
  infiniteScroll,
657
661
  infiniteScrollLoadMore: infiniteScroll && !!content.infiniteScrollLoadMore,
658
662
  pageUrlParamKey,
663
+ itemsPath: extractItemsPath(
664
+ content.source,
665
+ parentDataSource.identifier,
666
+ parentDataSource.resourceDef.dataSourceType || ''
667
+ ),
659
668
  category: 'plain',
660
669
  }
661
670
 
@@ -1410,6 +1419,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1410
1419
  }
1411
1420
  // Add filters to count fetch params if present
1412
1421
  appendFiltersParam(urlParams, usage.filters, buildFilterDestinationExpression)
1422
+ appendItemsPathParam(urlParams, usage.itemsPath, 'collectionPath')
1413
1423
 
1414
1424
  const countEffectDeps: types.Expression[] = [
1415
1425
  types.memberExpression(
@@ -1526,6 +1536,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1526
1536
  )
1527
1537
  }
1528
1538
  appendFiltersParam(countUrlParams, usage.filters, buildFilterDestinationExpression)
1539
+ appendItemsPathParam(countUrlParams, usage.itemsPath, 'collectionPath')
1529
1540
 
1530
1541
  // Default to mount-only; refresh when ANY filter destination
1531
1542
  // changes — state-bound (e.g. `selectedCategory`) and URL-driven —
@@ -2364,6 +2375,7 @@ function updateDataProviderForPaginatedSearch(
2364
2375
 
2365
2376
  // Add filters if present
2366
2377
  appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
2378
+ appendItemsPathParam(paramsProps, usage.itemsPath, 'itemsPath')
2367
2379
 
2368
2380
  // Build useMemo dependencies including filter state IDs and dynamic sort state IDs
2369
2381
  const memoDeps: types.Expression[] = [types.identifier(vars.combinedStateVar)]
@@ -2523,6 +2535,7 @@ function updateDataProviderForPaginationOnly(
2523
2535
 
2524
2536
  // Add filters if present
2525
2537
  appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
2538
+ appendItemsPathParam(paramsProps, usage.itemsPath, 'itemsPath')
2526
2539
 
2527
2540
  // Build useMemo dependencies including filter state IDs and dynamic sort state IDs
2528
2541
  const memoDeps: types.Expression[] = [types.identifier(vars.pageStateVar)]
@@ -2643,6 +2656,7 @@ function updateDataProviderForSearchOnly(
2643
2656
 
2644
2657
  // Add filters if present
2645
2658
  appendFiltersParam(paramsProps, usage.filters, buildFilterDestinationExpression)
2659
+ appendItemsPathParam(paramsProps, usage.itemsPath, 'itemsPath')
2646
2660
 
2647
2661
  // Build useMemo dependencies including filter state IDs and dynamic sort state IDs
2648
2662
  const memoDeps: types.Expression[] = [types.identifier(vars.debouncedSearchQueryVar)]
@@ -3668,6 +3682,8 @@ function updateGetStaticProps(
3668
3682
  )
3669
3683
  }
3670
3684
 
3685
+ appendItemsPathParam(fetchParams, usage.itemsPath, 'itemsPath')
3686
+
3671
3687
  // The prefetch is per locale — getStaticProps runs once per language.
3672
3688
  if (localized) {
3673
3689
  fetchParams.push(buildServerLocaleParam())
@@ -3749,8 +3765,10 @@ function updateGetStaticProps(
3749
3765
  const staticFiltersForCount = (usage.filters || []).filter(
3750
3766
  (f: any) => !ASTUtils.isUIDLDynamicReference(f.destination)
3751
3767
  )
3752
- const hasStaticFilters = staticFiltersForCount.length > 0
3753
- const countVarName = hasStaticFilters
3768
+ // A count scoped to an inner array is a different number from the
3769
+ // whole-payload one, so it gets its own variable just like a filtered one.
3770
+ const hasScopedCount = staticFiltersForCount.length > 0 || !!usage.itemsPath
3771
+ const countVarName = hasScopedCount
3754
3772
  ? `${usage.dataSourceIdentifier}_ds_${usage.index}_count`
3755
3773
  : `${usage.dataSourceIdentifier}_count`
3756
3774
 
@@ -3822,7 +3840,7 @@ function updateGetStaticProps(
3822
3840
  // Group usages by their filters (stringify for comparison)
3823
3841
  const usagesByFilters = new Map<string, DataSourceUsage[]>()
3824
3842
  for (const usage of usagesForDataSource) {
3825
- const filtersKey = JSON.stringify(usage.filters || [])
3843
+ const filtersKey = JSON.stringify([usage.filters || [], usage.itemsPath || []])
3826
3844
  const existing = usagesByFilters.get(filtersKey) || []
3827
3845
  existing.push(usage)
3828
3846
  usagesByFilters.set(filtersKey, existing)
@@ -3837,10 +3855,12 @@ function updateGetStaticProps(
3837
3855
  )
3838
3856
  const hasStaticFilters = staticFiltersForCount.length > 0
3839
3857
 
3840
- // Create unique count variable name based on filters
3841
- const countVarName = hasStaticFilters
3842
- ? `${firstUsage.dataSourceIdentifier}_ds_${firstUsage.index}_count`
3843
- : `${firstUsage.dataSourceIdentifier}_count`
3858
+ // Create unique count variable name based on filters (and inner-array
3859
+ // scope — must match the naming at the maxPages site above)
3860
+ const countVarName =
3861
+ hasStaticFilters || firstUsage.itemsPath
3862
+ ? `${firstUsage.dataSourceIdentifier}_ds_${firstUsage.index}_count`
3863
+ : `${firstUsage.dataSourceIdentifier}_count`
3844
3864
 
3845
3865
  // Check if this count was already processed
3846
3866
  const countKey = `${key}:${filtersKey}`
@@ -3892,6 +3912,8 @@ function updateGetStaticProps(
3892
3912
  )
3893
3913
  }
3894
3914
 
3915
+ appendItemsPathParam(countParams, firstUsage.itemsPath, 'collectionPath')
3916
+
3895
3917
  fetchesArray.elements.push(
3896
3918
  types.callExpression(
3897
3919
  types.memberExpression(
@@ -0,0 +1,74 @@
1
+ /**
2
+ * How many posts each direction is read, so a neighbour that only REDIRECTS
3
+ * elsewhere can be stepped over without a second query.
4
+ */
5
+ const ADJACENT_POST_CANDIDATES = 5
6
+
7
+ /**
8
+ * Generates the code that finds a post's neighbours for its "previous / next"
9
+ * navigation: the published posts right before and after it in the order the
10
+ * blog listing shows them (`created_at`, then `id`, newest first). "Previous"
11
+ * is the older one.
12
+ *
13
+ * Only a post DETAILS fetch (a single record) asks. A neighbour whose row
14
+ * carries a redirect is skipped — linking to it would only bounce the reader
15
+ * somewhere else — which is read off the row in code rather than in SQL, so a
16
+ * table provisioned before the SEO columns still works.
17
+ *
18
+ * MUST mirror `findAdjacentBlogPosts` in teleport-gui
19
+ * `packages/renderer/src/utils/blog-adjacent-posts.ts` (the canvas).
20
+ */
21
+ export const generateAdjacentPostsCode = (): string => `
22
+ var BLOG_ADJACENT_CANDIDATES = ${ADJACENT_POST_CANDIDATES}
23
+
24
+ function blogAdjacentPostsSql(comparison, direction) {
25
+ return (
26
+ 'SELECT p.* FROM teleport_blog_posts p,' +
27
+ ' (SELECT created_at, id FROM teleport_blog_posts WHERE id = $1) cur' +
28
+ " WHERE p.status = 'published' AND (p.created_at, p.id) " + comparison + ' (cur.created_at, cur.id)' +
29
+ ' ORDER BY p.created_at ' + direction + ', p.id ' + direction +
30
+ ' LIMIT ' + BLOG_ADJACENT_CANDIDATES
31
+ )
32
+ }
33
+
34
+ function firstLinkableBlogPost(result) {
35
+ var rows = result && Array.isArray(result.rows) ? result.rows : []
36
+ for (var i = 0; i < rows.length; i++) {
37
+ var row = rows[i]
38
+ if (!row) continue
39
+ var redirect = pickFirst(row.redirect_url, row.redirectUrl)
40
+ if (typeof redirect === 'string' && redirect.trim() !== '') continue
41
+ // Through JSON, like the rows the fetcher itself hands the transform.
42
+ return JSON.parse(JSON.stringify(row))
43
+ }
44
+ return null
45
+ }
46
+
47
+ // { [postId]: { previous, next } } — raw rows or null — for a single-record
48
+ // fetch; {} otherwise and on any failure (the navigation then hides).
49
+ async function getAdjacentPostsMap(getClientFn, records) {
50
+ var map = {}
51
+ if (!Array.isArray(records) || records.length !== 1) return map
52
+ var postId = records[0] && records[0].id
53
+ if (postId === null || postId === undefined) return map
54
+
55
+ var client
56
+ try {
57
+ client = getClientFn()
58
+ await client.connect()
59
+ var older = await client.query(blogAdjacentPostsSql('<', 'DESC'), [String(postId)])
60
+ var newer = await client.query(blogAdjacentPostsSql('>', 'ASC'), [String(postId)])
61
+ map[String(postId)] = {
62
+ previous: firstLinkableBlogPost(older),
63
+ next: firstLinkableBlogPost(newer),
64
+ }
65
+ } catch (e) {
66
+ // Leaves the navigation hidden.
67
+ } finally {
68
+ if (client) {
69
+ try { await client.end() } catch (e) { /* ignore */ }
70
+ }
71
+ }
72
+ return map
73
+ }
74
+ `
@@ -0,0 +1,132 @@
1
+ /**
2
+ * How many TOP-LEVEL comments a post page carries: the newest ones, shown
3
+ * oldest-first, each with all of its replies. The count in the section's
4
+ * heading still counts every approved comment.
5
+ *
6
+ * ⛔ PAIRED with `BLOG_COMMENTS_RENDER_LIMIT` in teleport-gui
7
+ * `features/blog/constants/blog-comments.ts` (the canvas mirror).
8
+ */
9
+ export const BLOG_COMMENTS_RENDER_LIMIT = 200
10
+
11
+ /**
12
+ * Generates the code that loads and shapes a post's approved comments.
13
+ *
14
+ * Read server-side, with the post: only a post DETAILS fetch (a single record)
15
+ * asks, so a listing never pays for it, and the comments arrive in the page's
16
+ * HTML — rendered by SSR/ISR, carried along a client-side navigation to another
17
+ * post, no request of their own.
18
+ *
19
+ * Public columns only. `teleport_blog_comments` holds the commenters' email
20
+ * addresses and the comments still waiting for approval, which is why it is a
21
+ * customer-record table no browser may read (see teleport-shared's
22
+ * `table-access.ts`); nothing here selects either.
23
+ *
24
+ * Replies are one level deep: a reply whose parent is not among the rows (not
25
+ * approved, deleted) is dropped rather than promoted to the top level.
26
+ *
27
+ * MUST mirror `buildBlogComments` in teleport-gui
28
+ * `packages/renderer/src/utils/blog-comments.ts` (the canvas).
29
+ */
30
+ export const generateBlogCommentsCode = (): string => `
31
+ var BLOG_COMMENTS_RENDER_LIMIT = ${BLOG_COMMENTS_RENDER_LIMIT}
32
+
33
+ var BLOG_COMMENTS_SQL =
34
+ 'WITH top AS (' +
35
+ ' SELECT id FROM teleport_blog_comments' +
36
+ " WHERE post_id = $1 AND parent_id IS NULL AND status = 'approved'" +
37
+ ' ORDER BY created_at DESC, id DESC LIMIT ' + BLOG_COMMENTS_RENDER_LIMIT +
38
+ ')' +
39
+ ' SELECT c.id, c.parent_id, c.author_name, c.content, c.is_author_reply, c.created_at' +
40
+ ' FROM teleport_blog_comments c' +
41
+ " WHERE c.post_id = $1 AND c.status = 'approved'" +
42
+ ' AND (c.id IN (SELECT id FROM top) OR c.parent_id IN (SELECT id FROM top))' +
43
+ ' ORDER BY c.created_at ASC, c.id ASC'
44
+
45
+ // Every approved comment the page could show — a reply counts only while the
46
+ // comment it answers is approved too.
47
+ var BLOG_COMMENTS_COUNT_SQL =
48
+ 'SELECT COUNT(*) AS n FROM teleport_blog_comments c' +
49
+ " WHERE c.post_id = $1 AND c.status = 'approved'" +
50
+ ' AND (c.parent_id IS NULL OR EXISTS (' +
51
+ ' SELECT 1 FROM teleport_blog_comments p' +
52
+ " WHERE p.id = c.parent_id AND p.status = 'approved'))"
53
+
54
+ function blogAuthorInitials(name) {
55
+ if (typeof name !== 'string') return ''
56
+ var words = name.trim().split(' ').filter(function(word) { return word !== '' })
57
+ if (words.length === 0) return ''
58
+ // Code points, not UTF-16 units: an initial may be an emoji or a letter
59
+ // outside the BMP, and half of a surrogate pair renders as a box.
60
+ var first = Array.from(words[0])[0] || ''
61
+ var last = words.length > 1 ? Array.from(words[words.length - 1])[0] || '' : ''
62
+ return (first + last).toUpperCase()
63
+ }
64
+
65
+ function buildBlogComment(row) {
66
+ var name = typeof row.author_name === 'string' ? row.author_name.trim() : ''
67
+ var authorName = name || 'Reader'
68
+ return {
69
+ id: String(row.id),
70
+ authorName: authorName,
71
+ authorInitials: blogAuthorInitials(authorName),
72
+ content: typeof row.content === 'string' ? row.content : '',
73
+ createdAt: normalizeTimestamp(row.created_at),
74
+ isAuthorReply: coerceBoolean(row.is_author_reply, false),
75
+ }
76
+ }
77
+
78
+ function isTopLevelComment(row) {
79
+ return row.parent_id === null || row.parent_id === undefined || row.parent_id === ''
80
+ }
81
+
82
+ function buildBlogComments(rows) {
83
+ var comments = []
84
+ if (!Array.isArray(rows)) return comments
85
+ var byId = {}
86
+ for (var i = 0; i < rows.length; i++) {
87
+ var row = rows[i]
88
+ if (!row || row.id == null || !isTopLevelComment(row)) continue
89
+ var comment = buildBlogComment(row)
90
+ comment.replies = []
91
+ byId[comment.id] = comment
92
+ comments.push(comment)
93
+ }
94
+ for (var j = 0; j < rows.length; j++) {
95
+ var reply = rows[j]
96
+ if (!reply || reply.id == null || isTopLevelComment(reply)) continue
97
+ var parent = byId[String(reply.parent_id)]
98
+ if (parent) parent.replies.push(buildBlogComment(reply))
99
+ }
100
+ return comments
101
+ }
102
+
103
+ // { [postId]: { rows, count } } for a single-record (details) fetch; {} for
104
+ // anything else, and on any failure — a blog whose comments table does not
105
+ // exist yet renders its posts without comments rather than failing the page.
106
+ async function getBlogCommentsMap(getClientFn, records) {
107
+ var map = {}
108
+ if (!Array.isArray(records) || records.length !== 1) return map
109
+ var postId = records[0] && records[0].id
110
+ if (postId === null || postId === undefined) return map
111
+
112
+ var client
113
+ try {
114
+ client = getClientFn()
115
+ await client.connect()
116
+ var listed = await client.query(BLOG_COMMENTS_SQL, [String(postId)])
117
+ var counted = await client.query(BLOG_COMMENTS_COUNT_SQL, [String(postId)])
118
+ // Through JSON, like every other row the fetcher hands the transform: the
119
+ // driver returns timestamps as Date objects.
120
+ var rows = listed && Array.isArray(listed.rows) ? JSON.parse(JSON.stringify(listed.rows)) : []
121
+ var count = counted && counted.rows && counted.rows[0] ? Number(counted.rows[0].n) : NaN
122
+ map[String(postId)] = { rows: rows, count: isNaN(count) ? rows.length : count }
123
+ } catch (e) {
124
+ // Leaves the post without comments.
125
+ } finally {
126
+ if (client) {
127
+ try { await client.end() } catch (e) { /* ignore */ }
128
+ }
129
+ }
130
+ return map
131
+ }
132
+ `
@@ -1,10 +1,15 @@
1
1
  import type { UIDLEcommerceCategory } from '@teleporthq/teleport-types'
2
2
  import { generateCategoryTaxonomyCode } from './category-taxonomy'
3
+ import { generateAdjacentPostsCode } from './blog-adjacent-posts'
4
+ import { generateBlogCommentsCode } from './blog-comments'
5
+ import { generateHeadingAnchorsCode } from './heading-anchors'
3
6
 
4
7
  /** What the blog-post transform bakes in at export time beyond the row itself. */
5
8
  export interface BlogPostTransformOptions {
6
9
  /** Category taxonomy — lives only in the UIDL, there is no DB table for it. */
7
10
  categories?: UIDLEcommerceCategory[]
11
+ /** Content headings get ids and `#` links — see `heading-anchors.ts`. */
12
+ headingAnchors?: boolean
8
13
  }
9
14
 
10
15
  /**
@@ -20,6 +25,10 @@ export const generateBlogPostTransformationCode = (
20
25
  // Blog Post Transformation
21
26
  // ============================================================
22
27
  ${generateCategoryTaxonomyCode('BLOG_CATEGORIES_BY_ID', options.categories)}
28
+ ${generateBlogCommentsCode()}
29
+ ${generateAdjacentPostsCode()}
30
+ ${generateHeadingAnchorsCode()}
31
+ var BLOG_HEADING_ANCHORS = ${options.headingAnchors === true}
23
32
 
24
33
  function buildBlogPost(record, options) {
25
34
  if (!record || typeof record !== 'object') return record
@@ -34,6 +43,7 @@ function buildBlogPost(record, options) {
34
43
  var title = resolveI18nField(record, 'title', 'title', currentLang, mainLang) || ''
35
44
  var slug = resolveI18nField(record, 'slug', 'slug', currentLang, mainLang) || ''
36
45
  var content = resolveI18nField(record, 'content', 'content', currentLang, mainLang) || ''
46
+ if (BLOG_HEADING_ANCHORS) content = addHeadingAnchors(content)
37
47
  var excerpt = resolveI18nField(record, 'excerpt', 'excerpt', currentLang, mainLang) || ''
38
48
  var category = resolveI18nField(record, 'category', 'category', currentLang, mainLang) || null
39
49
 
@@ -53,6 +63,7 @@ function buildBlogPost(record, options) {
53
63
  var metaTitle = resolveI18nField(record, 'meta_title', 'metaTitle', currentLang, mainLang) || null
54
64
  var metaDescription = resolveI18nField(record, 'meta_description', 'metaDescription', currentLang, mainLang) || null
55
65
  var featuredImageAlt = resolveI18nField(record, 'featured_image_alt', 'featuredImageAlt', currentLang, mainLang) || null
66
+ var authorBio = resolveI18nField(record, 'author_bio', 'authorBio', currentLang, mainLang) || null
56
67
 
57
68
  // Status
58
69
  var status = record.status || 'draft'
@@ -78,6 +89,7 @@ function buildBlogPost(record, options) {
78
89
 
79
90
  // Simple pass-through fields
80
91
  var authorName = pickFirst(record.author_name, record.authorName)
92
+ var authorInitials = blogAuthorInitials(authorName)
81
93
  var authorEmail = pickFirst(record.author_email, record.authorEmail)
82
94
  var readingTimeMinutes = safeNumber(pickFirst(record.reading_time_minutes, record.readingTimeMinutes), null)
83
95
 
@@ -132,12 +144,14 @@ function buildBlogPost(record, options) {
132
144
  var relatedPostIds = parseRelatedIds(record.related_post_ids)
133
145
  var relatedPostsById = options.relatedPostsById
134
146
  var relatedPosts = []
147
+ // The nested builds of the related and adjacent posts get NO maps: each is a
148
+ // card, not a page, and two posts that point at each other would recurse.
149
+ var relatedNestedOptions = {
150
+ assetMap: assetMap,
151
+ currentLanguage: currentLang,
152
+ mainLanguage: mainLang,
153
+ }
135
154
  if (relatedPostsById && relatedPostIds.length > 0) {
136
- var relatedNestedOptions = {
137
- assetMap: assetMap,
138
- currentLanguage: currentLang,
139
- mainLanguage: mainLang,
140
- }
141
155
  for (var rp = 0; rp < relatedPostIds.length; rp++) {
142
156
  var relatedId = relatedPostIds[rp]
143
157
  if (id != null && relatedId === String(id)) continue
@@ -150,6 +164,19 @@ function buildBlogPost(record, options) {
150
164
  }
151
165
  }
152
166
 
167
+ // The previous (older) and next (newer) published post, as 0-or-1-item
168
+ // lists: the post navigation maps over them like the related rail does, so an
169
+ // end of the blog simply renders no card on that side.
170
+ var adjacent = options.adjacentPostsById && id != null ? options.adjacentPostsById[String(id)] : null
171
+ var previousPosts = adjacent && adjacent.previous ? [buildBlogPost(adjacent.previous, relatedNestedOptions)] : []
172
+ var nextPosts = adjacent && adjacent.next ? [buildBlogPost(adjacent.next, relatedNestedOptions)] : []
173
+
174
+ // The approved comments — the newest top-level ones, each with its replies —
175
+ // present only on a details fetch; see blog-comments.ts.
176
+ var commentsEntry = options.commentsByPostId && id != null ? options.commentsByPostId[String(id)] : null
177
+ var comments = commentsEntry ? buildBlogComments(commentsEntry.rows) : []
178
+ var commentsCount = commentsEntry ? commentsEntry.count : 0
179
+
153
180
  // Timestamps
154
181
  var rawPublishedAt = pickFirst(record.published_at, record.publishedAt)
155
182
  var publishedAt = rawPublishedAt != null ? normalizeTimestamp(rawPublishedAt) : null
@@ -184,11 +211,17 @@ function buildBlogPost(record, options) {
184
211
  author_name: authorName,
185
212
  author_email: authorEmail,
186
213
  authorAvatarUrl: authorAvatarUrl,
214
+ authorBio: authorBio,
215
+ authorInitials: authorInitials,
187
216
  metaTitle: metaTitle,
188
217
  metaDescription: metaDescription,
189
218
  readingTimeMinutes: readingTimeMinutes,
190
219
  isFeatured: isFeatured,
191
220
  allowComments: allowComments,
221
+ comments: comments,
222
+ commentsCount: commentsCount,
223
+ previousPosts: previousPosts,
224
+ nextPosts: nextPosts,
192
225
  noIndex: noIndex,
193
226
  canonicalUrl: canonicalUrl,
194
227
  redirectUrl: redirectUrl,