@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,85 @@
1
+ /**
2
+ * The class of the `#` link added to a post's content headings.
3
+ *
4
+ * ⛔ PAIRED with `BLOG_HEADING_ANCHOR_CLASS` in teleport-gui
5
+ * `features/blog/constants/blog-heading-anchors.ts`, whose stylesheet (shipped
6
+ * on the post page) is what keeps the link out of sight until the heading is
7
+ * hovered or focused.
8
+ */
9
+ export const BLOG_HEADING_ANCHOR_CLASS = 'tq-heading-anchor'
10
+
11
+ /**
12
+ * Generates `addHeadingAnchors(html)`: every `<h2>`–`<h6>` of a post's content
13
+ * gets an `id` (kept when it already has one, otherwise a slug of its text,
14
+ * unique within the post) and a trailing `#` link to itself, so a section can
15
+ * be linked to (`/blog/post#setup`) and the link works on the very first load —
16
+ * the ids are in the server-rendered HTML.
17
+ *
18
+ * The link repeats the heading a screen reader already announces, so it is
19
+ * hidden from assistive technology and left out of the tab order; the heading
20
+ * stays the landmark.
21
+ *
22
+ * Idempotent: a heading that already carries the link is left alone. `h1` is
23
+ * the post title's level and is never touched.
24
+ */
25
+ export const generateHeadingAnchorsCode = (): string => `
26
+ var BLOG_HEADING_ANCHOR_CLASS = '${BLOG_HEADING_ANCHOR_CLASS}'
27
+ var BLOG_HEADING_ID_PATTERN = /\\sid\\s*=\\s*("([^"]*)"|'([^']*)')/i
28
+
29
+ function escapeHeadingAttribute(value) {
30
+ return String(value)
31
+ .replace(/&/g, '&amp;')
32
+ .replace(/"/g, '&quot;')
33
+ .replace(/</g, '&lt;')
34
+ .replace(/>/g, '&gt;')
35
+ }
36
+
37
+ function blogHeadingSlug(text, used) {
38
+ var base = String(text || '')
39
+ .normalize('NFKD')
40
+ .replace(/[\\u0300-\\u036f]/g, '')
41
+ .toLowerCase()
42
+ .replace(/[^\\p{L}\\p{N}]+/gu, '-')
43
+ .replace(/^-+|-+$/g, '')
44
+ if (!base) base = 'section'
45
+ var slug = base
46
+ var suffix = 2
47
+ while (used[slug]) {
48
+ slug = base + '-' + suffix
49
+ suffix++
50
+ }
51
+ used[slug] = true
52
+ return slug
53
+ }
54
+
55
+ function addHeadingAnchors(html) {
56
+ if (typeof html !== 'string' || !/<h[2-6][\\s>]/i.test(html)) return html
57
+ // Every id the content already uses is taken, so a generated one never
58
+ // duplicates it.
59
+ var used = {}
60
+ var existingIds = new RegExp(BLOG_HEADING_ID_PATTERN.source, 'gi')
61
+ var existing
62
+ while ((existing = existingIds.exec(html)) !== null) {
63
+ used[decodeHtmlEntities(existing[2] || existing[3] || '')] = true
64
+ }
65
+
66
+ return html.replace(/<h([2-6])(\\s[^>]*)?>([\\s\\S]*?)<\\/h\\1>/gi, function(match, level, rawAttrs, inner) {
67
+ if (inner.indexOf(BLOG_HEADING_ANCHOR_CLASS) !== -1) return match
68
+ var text = stripHtmlTags(inner)
69
+ if (!text) return match
70
+ var attrs = rawAttrs || ''
71
+ var idMatch = attrs.match(BLOG_HEADING_ID_PATTERN)
72
+ var id = idMatch ? decodeHtmlEntities(idMatch[2] || idMatch[3] || '') : ''
73
+ if (!id) {
74
+ id = blogHeadingSlug(text, used)
75
+ attrs = ' id="' + escapeHeadingAttribute(id) + '"' + attrs
76
+ }
77
+ return (
78
+ '<h' + level + attrs + '>' + inner +
79
+ '<a class="' + BLOG_HEADING_ANCHOR_CLASS + '" href="#' + escapeHeadingAttribute(id) +
80
+ '" aria-hidden="true" tabindex="-1">#</a>' +
81
+ '</h' + level + '>'
82
+ )
83
+ })
84
+ }
85
+ `
@@ -1,5 +1,9 @@
1
- import type { GeneratorOptions, UIDLEcommerceCategory } from '@teleporthq/teleport-types'
2
- import { StorefrontTax } from '@teleporthq/teleport-shared'
1
+ import type {
2
+ GeneratorOptions,
3
+ UIDLAuthentication,
4
+ UIDLEcommerceCategory,
5
+ } from '@teleporthq/teleport-types'
6
+ import { StorefrontTax, TableAccess } from '@teleporthq/teleport-shared'
3
7
  import { generateSharedTransformationCode } from './shared-utils'
4
8
  import { generateBlogPostTransformationCode } from './blog-post'
5
9
  import { generateCustomPageTransformationCode } from './custom-page'
@@ -25,6 +29,14 @@ export type { EcommerceProductTransformOptions }
25
29
  export interface EntityTransformOptions extends EcommerceProductTransformOptions {
26
30
  /** Blog post-category taxonomy — see `blogSettings.categories`. */
27
31
  blogCategories?: UIDLEcommerceCategory[]
32
+ /** Post content headings get ids and `#` links — see `blogSettings.headingAnchors`. */
33
+ blogHeadingAnchors?: boolean
34
+ /**
35
+ * A post details fetch carries the post's approved comments — see
36
+ * `blogSettings.comments`. Off, the comments table is never queried: a blog
37
+ * without comments has no such table.
38
+ */
39
+ blogComments?: boolean
28
40
  /**
29
41
  * The languages the rows are stored in — absent for a single-language
30
42
  * project. The transform resolves every translatable field against the
@@ -32,6 +44,12 @@ export interface EntityTransformOptions extends EcommerceProductTransformOptions
32
44
  * the fetcher matches and sorts translatable columns the same way.
33
45
  */
34
46
  localization?: ContentLocalization
47
+ /**
48
+ * Who may read a money table (gift cards, vouchers, their ledgers) from a
49
+ * browser: the roles the generated admin panel admits. See
50
+ * `TableAccess.resolveTrustedReaderRoles`.
51
+ */
52
+ trustedReaderRoles?: string[]
35
53
  }
36
54
 
37
55
  /**
@@ -60,14 +78,32 @@ const resolveAllowBackorders = (
60
78
  * "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
61
79
  */
62
80
  export const buildProductTransformOptions = (
63
- options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings'> &
64
- LocalizedProjectOptions
81
+ options: Pick<
82
+ GeneratorOptions,
83
+ 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings' | 'auth'
84
+ > &
85
+ LocalizedProjectOptions & {
86
+ /**
87
+ * The SAME project authentication under the name a `ProjectUIDL` gives
88
+ * it — the project plugins call this with the UIDL itself, where the
89
+ * field is `authentication`, while a component plugin calls it with
90
+ * `GeneratorOptions`, where it is `auth`. Reading only one of the two
91
+ * left every fetcher emitted by the project plugins with an empty
92
+ * trusted-reader list, i.e. a generated admin panel refused its own
93
+ * money tables.
94
+ */
95
+ authentication?: UIDLAuthentication
96
+ }
65
97
  ): EntityTransformOptions => ({
66
98
  categories: options.ecommerceSettings?.categories,
67
99
  blogCategories: options.blogSettings?.categories,
100
+ blogHeadingAnchors: options.blogSettings?.headingAnchors === true,
101
+ blogComments: options.blogSettings?.comments === true,
68
102
  storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
69
103
  allowBackorders: resolveAllowBackorders(options.ecommerceSettings),
104
+ productPages: options.ecommerceSettings?.productPages,
70
105
  localization: resolveContentLocalization(options),
106
+ trustedReaderRoles: TableAccess.resolveTrustedReaderRoles(options.auth || options.authentication),
71
107
  })
72
108
 
73
109
  export type TransformationType = 'blog-post' | 'ecommerce-product' | 'custom-page' | null
@@ -132,7 +168,13 @@ export const getTransformationCode = (
132
168
 
133
169
  switch (type) {
134
170
  case 'blog-post':
135
- return shared + generateBlogPostTransformationCode({ categories: options.blogCategories })
171
+ return (
172
+ shared +
173
+ generateBlogPostTransformationCode({
174
+ categories: options.blogCategories,
175
+ headingAnchors: options.blogHeadingAnchors,
176
+ })
177
+ )
136
178
  case 'ecommerce-product':
137
179
  return shared + generateEcommerceProductTransformationCode(options)
138
180
  case 'custom-page':
@@ -142,30 +184,99 @@ export const getTransformationCode = (
142
184
  }
143
185
  }
144
186
 
187
+ /**
188
+ * The request parameter that asks a transformed table for its rows AS STORED.
189
+ *
190
+ * The product, blog-post and custom-page transforms build a storefront view
191
+ * model: camelCased keys, flags as `"true"`/`"false"` strings, derived fields,
192
+ * and several columns left out altogether (a product's `discounts`,
193
+ * `category_ids`, `download_limit`, …). A caller that WRITES the row back —
194
+ * the generated admin's edit forms — needs the columns themselves: fed the view
195
+ * model, its form showed a digital product as not digital, a gift card switch
196
+ * ticked on a product that is not one, no discounts, and every save wrote the
197
+ * missing columns back blank.
198
+ *
199
+ * ⛔ PAIRED with `ADMIN_RAW_ROWS_PARAM` in the GUI's
200
+ * `mappers/domain-to-uidl/feature-detail-page-options.ts`, which bakes it into
201
+ * the admin update pages' detail resource.
202
+ */
203
+ export const RAW_ROWS_PARAM = 'rawRows'
204
+
145
205
  /**
146
206
  * Returns the JavaScript expression that transforms the data array.
147
207
  * This expression assumes the data array variable is called `safeData`,
148
208
  * that `getClient` is available for asset resolution, and that `req` is in scope.
149
- * Returns null if no transformation is needed.
209
+ * Returns null if no transformation is needed. A request carrying
210
+ * `RAW_ROWS_PARAM` gets the stored rows instead (see `rawRecords`).
150
211
  */
151
212
  export const getTransformExpression = (tableName: string): string | null => {
152
- const type = detectTransformationType(tableName)
153
- if (!type) {
213
+ if (!detectTransformationType(tableName)) {
154
214
  return null
155
215
  }
216
+ return '(__tqWantsRawRows(req.query) ? await rawRecords(safeData, getClient) : await transformRecords(safeData, getClient, req.query))'
217
+ }
156
218
 
157
- switch (type) {
158
- case 'blog-post':
159
- return 'await transformRecords(safeData, getClient, req.query)'
160
- case 'ecommerce-product':
161
- return 'await transformRecords(safeData, getClient, req.query)'
162
- case 'custom-page':
163
- return 'await transformRecords(safeData, getClient, req.query)'
164
- default:
165
- return null
219
+ /**
220
+ * The raw-rows mode every transformed table's fetcher carries: the rows as
221
+ * stored, with one exception — a value that is an uploaded asset's ID, alone or
222
+ * inside a JSON list column, becomes the URL the transform would have shown, so
223
+ * an edit form's image previews keep working. Nothing else is renamed, derived
224
+ * or dropped. The response cache keys on the query, so the two modes never
225
+ * share an entry.
226
+ */
227
+ const RAW_ROWS_CODE = `
228
+ function __tqWantsRawRows(query) {
229
+ var flag = query ? query.${RAW_ROWS_PARAM} : undefined
230
+ return flag === true || flag === 'true'
231
+ }
232
+
233
+ async function rawRecords(records, getClientFn) {
234
+ if (!Array.isArray(records)) return []
235
+ var assetMap = {}
236
+ try {
237
+ assetMap = await getAssetMap(getClientFn)
238
+ } catch (e) {
239
+ // Asset resolution is best-effort; the stored values pass through as they are.
240
+ }
241
+ return records.map(function(record) { return __tqResolveRowAssets(record, assetMap || {}) })
242
+ }
243
+
244
+ function __tqResolveRowAssets(record, assetMap) {
245
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return record
246
+ var out = {}
247
+ for (var key in record) {
248
+ if (!Object.prototype.hasOwnProperty.call(record, key)) continue
249
+ out[key] = __tqResolveAssetValue(record[key], assetMap)
166
250
  }
251
+ return out
167
252
  }
168
253
 
254
+ function __tqIsAssetId(value, assetMap) {
255
+ return typeof value === 'string' && value !== '' && Object.prototype.hasOwnProperty.call(assetMap, value)
256
+ }
257
+
258
+ function __tqResolveAssetValue(value, assetMap) {
259
+ if (typeof value !== 'string' || !value) return value
260
+ if (__tqIsAssetId(value, assetMap)) return resolveAssetUrl(value, assetMap)
261
+ var trimmed = value.trim()
262
+ if (trimmed.charAt(0) !== '[') return value
263
+ var list
264
+ try {
265
+ list = JSON.parse(trimmed)
266
+ } catch (e) {
267
+ return value
268
+ }
269
+ if (!Array.isArray(list)) return value
270
+ var changed = false
271
+ var resolved = list.map(function(item) {
272
+ if (!__tqIsAssetId(item, assetMap)) return item
273
+ changed = true
274
+ return resolveAssetUrl(item, assetMap)
275
+ })
276
+ return changed ? JSON.stringify(resolved) : value
277
+ }
278
+ `
279
+
169
280
  /**
170
281
  * How many reviews a product page carries into its structured data.
171
282
  *
@@ -189,7 +300,7 @@ export const REVIEWS_PER_PRODUCT = 5
189
300
  */
190
301
  export const getTransformWrapperCode = (
191
302
  tableName: string,
192
- options: Pick<EntityTransformOptions, 'localization'> = {}
303
+ options: Pick<EntityTransformOptions, 'localization' | 'blogComments'> = {}
193
304
  ): string => {
194
305
  const type = detectTransformationType(tableName)
195
306
  if (!type) {
@@ -273,9 +384,11 @@ export const getTransformWrapperCode = (
273
384
  }`
274
385
  : ''
275
386
 
387
+ // `includeOptionDetail` uses the same single-record heuristic as the related
388
+ // items: only the details page carries a product's full option tree.
276
389
  const variantOption =
277
390
  type === 'ecommerce-product'
278
- ? ', variantsByProductId: variantsByProductId, ratingsByProductId: ratingsByProductId, reviewsByProductId: reviewsByProductId'
391
+ ? ', variantsByProductId: variantsByProductId, ratingsByProductId: ratingsByProductId, reviewsByProductId: reviewsByProductId, includeOptionDetail: Array.isArray(records) && records.length === 1'
279
392
  : ''
280
393
 
281
394
  // Related items are resolved for a SINGLE-record fetch only — which is the
@@ -307,16 +420,46 @@ export const getTransformWrapperCode = (
307
420
  : ''
308
421
  const relatedOption = hasRelatedItems ? `, ${relatedMapVar}: ${relatedMapVar}` : ''
309
422
 
310
- return `
423
+ // A post page's neighbours and comments, on the same single-record heuristic
424
+ // as the related rail. The comments are only asked for when the blog takes
425
+ // them — see `EntityTransformOptions.blogComments`.
426
+ const blogPageEnrichment =
427
+ type === 'blog-post'
428
+ ? `
429
+ var adjacentPostsById = null
430
+ var commentsByPostId = null
431
+ if (Array.isArray(records) && records.length === 1) {
432
+ try {
433
+ adjacentPostsById = await getAdjacentPostsMap(getClientFn, records)
434
+ } catch (e) {
435
+ // Best-effort; the post navigation stays hidden.
436
+ }${
437
+ options.blogComments
438
+ ? `
439
+ try {
440
+ commentsByPostId = await getBlogCommentsMap(getClientFn, records)
441
+ } catch (e) {
442
+ // Best-effort; the post renders without comments.
443
+ }`
444
+ : ''
445
+ }
446
+ }`
447
+ : ''
448
+ const blogPageOption =
449
+ type === 'blog-post'
450
+ ? ', adjacentPostsById: adjacentPostsById, commentsByPostId: commentsByPostId'
451
+ : ''
452
+
453
+ return `${RAW_ROWS_CODE}
311
454
  async function transformRecords(records, getClientFn, reqQuery) {
312
455
  var assetMap = {}
313
456
  try {
314
457
  assetMap = await getAssetMap(getClientFn)
315
458
  } catch (e) {
316
459
  // Asset resolution is best-effort; continue without it
317
- }${relatedEnrichment}${variantEnrichment}
460
+ }${relatedEnrichment}${blogPageEnrichment}${variantEnrichment}
318
461
  var currentLanguage = reqQuery && typeof reqQuery.${REQUEST_LOCALE_PARAM} === 'string' && reqQuery.${REQUEST_LOCALE_PARAM} ? reqQuery.${REQUEST_LOCALE_PARAM} : null
319
- var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: ${mainLanguageLiteral}${variantOption}${relatedOption} }
462
+ var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: ${mainLanguageLiteral}${variantOption}${relatedOption}${blogPageOption} }
320
463
  return ${transformFn}(records, options)
321
464
  }
322
465
  `
@@ -74,6 +74,12 @@ function getCurrencySymbol(currencyCode) {
74
74
 
75
75
  function normalizeTimestamp(value) {
76
76
  if (value == null) return Date.now()
77
+ // A row read straight off the driver (the related / adjacent lookups) carries
78
+ // its timestamps as Date objects.
79
+ if (value instanceof Date) {
80
+ var time = value.getTime()
81
+ return isNaN(time) ? Date.now() : time
82
+ }
77
83
  if (typeof value === 'number') {
78
84
  if (isNaN(value)) return Date.now()
79
85
  return value < 100000000000 ? value * 1000 : value
package/src/utils.ts CHANGED
@@ -628,8 +628,8 @@ export const generateSafeJSONParseCode = (): string => {
628
628
  * collapsing to an empty result the caller cannot explain.
629
629
  */
630
630
  export const generateCollectionPathHelperCode = (): string => {
631
- return `const resolveCollectionPath = (payload, rawPath) => {
632
- if (!rawPath) return payload
631
+ return `const parsePayloadPath = (rawPath) => {
632
+ if (!rawPath) return null
633
633
  let path = rawPath
634
634
  if (typeof path === 'string') {
635
635
  try {
@@ -638,17 +638,39 @@ export const generateCollectionPathHelperCode = (): string => {
638
638
  path = path.split('.').filter(Boolean)
639
639
  }
640
640
  }
641
- if (!Array.isArray(path) || path.length === 0) return payload
641
+ return Array.isArray(path) && path.length > 0 ? path : null
642
+ }
642
643
 
644
+ const readPayloadPath = (payload, path) => {
643
645
  let current = payload
644
646
  for (const segment of path) {
645
647
  if (!current || typeof current !== 'object' || Array.isArray(current) || !(segment in current)) {
646
- return payload
648
+ return undefined
647
649
  }
648
650
  current = current[segment]
649
651
  }
652
+ return current
653
+ }
650
654
 
655
+ const resolveCollectionPath = (payload, rawPath) => {
656
+ const path = parsePayloadPath(rawPath)
657
+ if (!path) return payload
658
+ const current = readPayloadPath(payload, path)
651
659
  return Array.isArray(current) ? current : payload
660
+ }
661
+
662
+ // A list bound to an array INSIDE the payload (\`data?.results\`) still has to be
663
+ // searched, filtered, sorted and paged, but the page reads it back through the
664
+ // same path, so the wrapper must survive. Returns the inner array plus a way to
665
+ // put the processed array back, or null when the path does not lead to one.
666
+ const openItemsEnvelope = (payload, rawPath) => {
667
+ const path = parsePayloadPath(rawPath)
668
+ if (!path) return null
669
+ const items = readPayloadPath(payload, path)
670
+ 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) }
673
+ return { items, close: (next) => rebuild(payload, 0, next) }
652
674
  }`
653
675
  }
654
676