@teleporthq/teleport-plugin-next-data-source 0.43.66 → 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 (334) hide show
  1. package/__tests__/array-mapper-cache-wiring.test.ts +11 -0
  2. package/__tests__/array-overlap-sql.test.ts +53 -0
  3. package/__tests__/blog-post-page-enrichment.test.ts +214 -0
  4. package/__tests__/browser-row-policy.test.ts +503 -0
  5. package/__tests__/data-source-cache-wrapping.test.ts +18 -2
  6. package/__tests__/data-source-request-locale.test.ts +313 -0
  7. package/__tests__/ecommerce-product-options.test.ts +206 -0
  8. package/__tests__/ecommerce-product-pages.test.ts +82 -0
  9. package/__tests__/ecommerce-product-pre-options.golden.json +505 -0
  10. package/__tests__/ecommerce-product-storefront-contract.test.ts +293 -0
  11. package/__tests__/ecommerce-product-subscriptions.test.ts +121 -0
  12. package/__tests__/fetchers.test.ts +2 -2
  13. package/__tests__/gated-data-provider.test.ts +88 -0
  14. package/__tests__/items-path.test.ts +150 -0
  15. package/__tests__/localized-columns.test.ts +217 -0
  16. package/__tests__/numbered-pagination.test.ts +15 -0
  17. package/__tests__/product-filter-fields.ts +230 -0
  18. package/__tests__/product-filter-sql.fixture.json +7 -0
  19. package/__tests__/product-options-storefront.fixture.json +2510 -0
  20. package/__tests__/product-page-url-fixtures.json +25 -0
  21. package/__tests__/product-price-sort.ts +4 -2
  22. package/__tests__/raw-rows-mode.test.ts +111 -0
  23. package/__tests__/refetch-loading-state.test.ts +23 -0
  24. package/__tests__/rest-api-response-cache.test.ts +434 -0
  25. package/__tests__/search-clear-button.test.ts +122 -0
  26. package/__tests__/table-read-route-protected-tables.test.ts +493 -0
  27. package/dist/cjs/array-overlap-sql.d.ts +21 -0
  28. package/dist/cjs/array-overlap-sql.d.ts.map +1 -0
  29. package/dist/cjs/array-overlap-sql.js +25 -0
  30. package/dist/cjs/array-overlap-sql.js.map +1 -0
  31. package/dist/cjs/content-localization.d.ts +37 -0
  32. package/dist/cjs/content-localization.d.ts.map +1 -0
  33. package/dist/cjs/content-localization.js +29 -0
  34. package/dist/cjs/content-localization.js.map +1 -0
  35. package/dist/cjs/count-effect.d.ts +11 -1
  36. package/dist/cjs/count-effect.d.ts.map +1 -1
  37. package/dist/cjs/count-effect.js +30 -2
  38. package/dist/cjs/count-effect.js.map +1 -1
  39. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  40. package/dist/cjs/data-source-fetchers.js +10 -4
  41. package/dist/cjs/data-source-fetchers.js.map +1 -1
  42. package/dist/cjs/fetchers/airtable.d.ts.map +1 -1
  43. package/dist/cjs/fetchers/airtable.js +1 -1
  44. package/dist/cjs/fetchers/airtable.js.map +1 -1
  45. package/dist/cjs/fetchers/clickhouse.d.ts.map +1 -1
  46. package/dist/cjs/fetchers/clickhouse.js +1 -1
  47. package/dist/cjs/fetchers/clickhouse.js.map +1 -1
  48. package/dist/cjs/fetchers/firestore.d.ts.map +1 -1
  49. package/dist/cjs/fetchers/firestore.js +1 -1
  50. package/dist/cjs/fetchers/firestore.js.map +1 -1
  51. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  52. package/dist/cjs/fetchers/javascript.js +1 -1
  53. package/dist/cjs/fetchers/javascript.js.map +1 -1
  54. package/dist/cjs/fetchers/mariadb.d.ts.map +1 -1
  55. package/dist/cjs/fetchers/mariadb.js +1 -1
  56. package/dist/cjs/fetchers/mariadb.js.map +1 -1
  57. package/dist/cjs/fetchers/mongodb.d.ts.map +1 -1
  58. package/dist/cjs/fetchers/mongodb.js +1 -1
  59. package/dist/cjs/fetchers/mongodb.js.map +1 -1
  60. package/dist/cjs/fetchers/mysql.d.ts.map +1 -1
  61. package/dist/cjs/fetchers/mysql.js +1 -1
  62. package/dist/cjs/fetchers/mysql.js.map +1 -1
  63. package/dist/cjs/fetchers/postgresql.d.ts +1 -1
  64. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  65. package/dist/cjs/fetchers/postgresql.js +9 -4
  66. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  67. package/dist/cjs/fetchers/raw-query.d.ts +8 -1
  68. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  69. package/dist/cjs/fetchers/raw-query.js +42 -4
  70. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  71. package/dist/cjs/fetchers/redshift.d.ts.map +1 -1
  72. package/dist/cjs/fetchers/redshift.js +1 -1
  73. package/dist/cjs/fetchers/redshift.js.map +1 -1
  74. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  75. package/dist/cjs/fetchers/rest-api.js +7 -2
  76. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  77. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  78. package/dist/cjs/fetchers/teleport.js +16 -6
  79. package/dist/cjs/fetchers/teleport.js.map +1 -1
  80. package/dist/cjs/fetchers/turso.d.ts.map +1 -1
  81. package/dist/cjs/fetchers/turso.js +1 -1
  82. package/dist/cjs/fetchers/turso.js.map +1 -1
  83. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +43 -0
  84. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  85. package/dist/cjs/fetchers/utils/browser-row-policy.js +66 -0
  86. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -0
  87. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts +8 -0
  88. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  89. package/dist/cjs/fetchers/utils/request-sql-guards.js +12 -0
  90. package/dist/cjs/fetchers/utils/request-sql-guards.js.map +1 -0
  91. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  92. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  93. package/dist/cjs/fetchers/utils/rest-api-response-cache.js +64 -0
  94. package/dist/cjs/fetchers/utils/rest-api-response-cache.js.map +1 -0
  95. package/dist/cjs/fetchers/utils/table-access-guard.d.ts +23 -0
  96. package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -0
  97. package/dist/cjs/fetchers/utils/table-access-guard.js +34 -0
  98. package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -0
  99. package/dist/cjs/index.d.ts +5 -0
  100. package/dist/cjs/index.d.ts.map +1 -1
  101. package/dist/cjs/index.js +60 -8
  102. package/dist/cjs/index.js.map +1 -1
  103. package/dist/cjs/items-path.d.ts +10 -0
  104. package/dist/cjs/items-path.d.ts.map +1 -0
  105. package/dist/cjs/items-path.js +86 -0
  106. package/dist/cjs/items-path.js.map +1 -0
  107. package/dist/cjs/loading-state.d.ts +16 -1
  108. package/dist/cjs/loading-state.d.ts.map +1 -1
  109. package/dist/cjs/loading-state.js +69 -3
  110. package/dist/cjs/loading-state.js.map +1 -1
  111. package/dist/cjs/localized-columns.d.ts +56 -0
  112. package/dist/cjs/localized-columns.d.ts.map +1 -0
  113. package/dist/cjs/localized-columns.js +137 -0
  114. package/dist/cjs/localized-columns.js.map +1 -0
  115. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  116. package/dist/cjs/pagination-plugin.js +140 -50
  117. package/dist/cjs/pagination-plugin.js.map +1 -1
  118. package/dist/cjs/product-filter-fields.d.ts +87 -0
  119. package/dist/cjs/product-filter-fields.d.ts.map +1 -0
  120. package/dist/cjs/product-filter-fields.js +161 -0
  121. package/dist/cjs/product-filter-fields.js.map +1 -0
  122. package/dist/cjs/request-locale.d.ts +49 -0
  123. package/dist/cjs/request-locale.d.ts.map +1 -0
  124. package/dist/cjs/request-locale.js +130 -0
  125. package/dist/cjs/request-locale.js.map +1 -0
  126. package/dist/cjs/search-clear.d.ts +31 -0
  127. package/dist/cjs/search-clear.d.ts.map +1 -0
  128. package/dist/cjs/search-clear.js +103 -0
  129. package/dist/cjs/search-clear.js.map +1 -0
  130. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +16 -0
  131. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -0
  132. package/dist/cjs/transformations/blog-adjacent-posts.js +25 -0
  133. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -0
  134. package/dist/cjs/transformations/blog-comments.d.ts +30 -0
  135. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -0
  136. package/dist/cjs/transformations/blog-comments.js +34 -0
  137. package/dist/cjs/transformations/blog-comments.js.map +1 -0
  138. package/dist/cjs/transformations/blog-post.d.ts +2 -0
  139. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  140. package/dist/cjs/transformations/blog-post.js +4 -1
  141. package/dist/cjs/transformations/blog-post.js.map +1 -1
  142. package/dist/cjs/transformations/ecommerce-product.d.ts +9 -1
  143. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  144. package/dist/cjs/transformations/ecommerce-product.js +28 -1
  145. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  146. package/dist/cjs/transformations/heading-anchors.d.ts +25 -0
  147. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -0
  148. package/dist/cjs/transformations/heading-anchors.js +29 -0
  149. package/dist/cjs/transformations/heading-anchors.js.map +1 -0
  150. package/dist/cjs/transformations/index.d.ts +61 -4
  151. package/dist/cjs/transformations/index.d.ts.map +1 -1
  152. package/dist/cjs/transformations/index.js +69 -19
  153. package/dist/cjs/transformations/index.js.map +1 -1
  154. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  155. package/dist/cjs/transformations/shared-utils.js +1 -1
  156. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  157. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  158. package/dist/cjs/utils.d.ts +11 -1
  159. package/dist/cjs/utils.d.ts.map +1 -1
  160. package/dist/cjs/utils.js +60 -16
  161. package/dist/cjs/utils.js.map +1 -1
  162. package/dist/esm/array-overlap-sql.d.ts +21 -0
  163. package/dist/esm/array-overlap-sql.d.ts.map +1 -0
  164. package/dist/esm/array-overlap-sql.js +21 -0
  165. package/dist/esm/array-overlap-sql.js.map +1 -0
  166. package/dist/esm/content-localization.d.ts +37 -0
  167. package/dist/esm/content-localization.d.ts.map +1 -0
  168. package/dist/esm/content-localization.js +24 -0
  169. package/dist/esm/content-localization.js.map +1 -0
  170. package/dist/esm/count-effect.d.ts +11 -1
  171. package/dist/esm/count-effect.d.ts.map +1 -1
  172. package/dist/esm/count-effect.js +28 -1
  173. package/dist/esm/count-effect.js.map +1 -1
  174. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  175. package/dist/esm/data-source-fetchers.js +11 -5
  176. package/dist/esm/data-source-fetchers.js.map +1 -1
  177. package/dist/esm/fetchers/airtable.d.ts.map +1 -1
  178. package/dist/esm/fetchers/airtable.js +1 -1
  179. package/dist/esm/fetchers/airtable.js.map +1 -1
  180. package/dist/esm/fetchers/clickhouse.d.ts.map +1 -1
  181. package/dist/esm/fetchers/clickhouse.js +1 -1
  182. package/dist/esm/fetchers/clickhouse.js.map +1 -1
  183. package/dist/esm/fetchers/firestore.d.ts.map +1 -1
  184. package/dist/esm/fetchers/firestore.js +1 -1
  185. package/dist/esm/fetchers/firestore.js.map +1 -1
  186. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  187. package/dist/esm/fetchers/javascript.js +1 -1
  188. package/dist/esm/fetchers/javascript.js.map +1 -1
  189. package/dist/esm/fetchers/mariadb.d.ts.map +1 -1
  190. package/dist/esm/fetchers/mariadb.js +1 -1
  191. package/dist/esm/fetchers/mariadb.js.map +1 -1
  192. package/dist/esm/fetchers/mongodb.d.ts.map +1 -1
  193. package/dist/esm/fetchers/mongodb.js +1 -1
  194. package/dist/esm/fetchers/mongodb.js.map +1 -1
  195. package/dist/esm/fetchers/mysql.d.ts.map +1 -1
  196. package/dist/esm/fetchers/mysql.js +1 -1
  197. package/dist/esm/fetchers/mysql.js.map +1 -1
  198. package/dist/esm/fetchers/postgresql.d.ts +1 -1
  199. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  200. package/dist/esm/fetchers/postgresql.js +9 -4
  201. package/dist/esm/fetchers/postgresql.js.map +1 -1
  202. package/dist/esm/fetchers/raw-query.d.ts +8 -1
  203. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  204. package/dist/esm/fetchers/raw-query.js +42 -4
  205. package/dist/esm/fetchers/raw-query.js.map +1 -1
  206. package/dist/esm/fetchers/redshift.d.ts.map +1 -1
  207. package/dist/esm/fetchers/redshift.js +1 -1
  208. package/dist/esm/fetchers/redshift.js.map +1 -1
  209. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  210. package/dist/esm/fetchers/rest-api.js +7 -2
  211. package/dist/esm/fetchers/rest-api.js.map +1 -1
  212. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  213. package/dist/esm/fetchers/teleport.js +16 -6
  214. package/dist/esm/fetchers/teleport.js.map +1 -1
  215. package/dist/esm/fetchers/turso.d.ts.map +1 -1
  216. package/dist/esm/fetchers/turso.js +1 -1
  217. package/dist/esm/fetchers/turso.js.map +1 -1
  218. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +43 -0
  219. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  220. package/dist/esm/fetchers/utils/browser-row-policy.js +60 -0
  221. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -0
  222. package/dist/esm/fetchers/utils/request-sql-guards.d.ts +8 -0
  223. package/dist/esm/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  224. package/dist/esm/fetchers/utils/request-sql-guards.js +8 -0
  225. package/dist/esm/fetchers/utils/request-sql-guards.js.map +1 -0
  226. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  227. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  228. package/dist/esm/fetchers/utils/rest-api-response-cache.js +59 -0
  229. package/dist/esm/fetchers/utils/rest-api-response-cache.js.map +1 -0
  230. package/dist/esm/fetchers/utils/table-access-guard.d.ts +23 -0
  231. package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -0
  232. package/dist/esm/fetchers/utils/table-access-guard.js +29 -0
  233. package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -0
  234. package/dist/esm/index.d.ts +5 -0
  235. package/dist/esm/index.d.ts.map +1 -1
  236. package/dist/esm/index.js +55 -7
  237. package/dist/esm/index.js.map +1 -1
  238. package/dist/esm/items-path.d.ts +10 -0
  239. package/dist/esm/items-path.d.ts.map +1 -0
  240. package/dist/esm/items-path.js +58 -0
  241. package/dist/esm/items-path.js.map +1 -0
  242. package/dist/esm/loading-state.d.ts +16 -1
  243. package/dist/esm/loading-state.d.ts.map +1 -1
  244. package/dist/esm/loading-state.js +69 -3
  245. package/dist/esm/loading-state.js.map +1 -1
  246. package/dist/esm/localized-columns.d.ts +56 -0
  247. package/dist/esm/localized-columns.d.ts.map +1 -0
  248. package/dist/esm/localized-columns.js +132 -0
  249. package/dist/esm/localized-columns.js.map +1 -0
  250. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  251. package/dist/esm/pagination-plugin.js +141 -51
  252. package/dist/esm/pagination-plugin.js.map +1 -1
  253. package/dist/esm/product-filter-fields.d.ts +87 -0
  254. package/dist/esm/product-filter-fields.d.ts.map +1 -0
  255. package/dist/esm/product-filter-fields.js +154 -0
  256. package/dist/esm/product-filter-fields.js.map +1 -0
  257. package/dist/esm/request-locale.d.ts +49 -0
  258. package/dist/esm/request-locale.d.ts.map +1 -0
  259. package/dist/esm/request-locale.js +98 -0
  260. package/dist/esm/request-locale.js.map +1 -0
  261. package/dist/esm/search-clear.d.ts +31 -0
  262. package/dist/esm/search-clear.d.ts.map +1 -0
  263. package/dist/esm/search-clear.js +74 -0
  264. package/dist/esm/search-clear.js.map +1 -0
  265. package/dist/esm/transformations/blog-adjacent-posts.d.ts +16 -0
  266. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -0
  267. package/dist/esm/transformations/blog-adjacent-posts.js +21 -0
  268. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -0
  269. package/dist/esm/transformations/blog-comments.d.ts +30 -0
  270. package/dist/esm/transformations/blog-comments.d.ts.map +1 -0
  271. package/dist/esm/transformations/blog-comments.js +30 -0
  272. package/dist/esm/transformations/blog-comments.js.map +1 -0
  273. package/dist/esm/transformations/blog-post.d.ts +2 -0
  274. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  275. package/dist/esm/transformations/blog-post.js +4 -1
  276. package/dist/esm/transformations/blog-post.js.map +1 -1
  277. package/dist/esm/transformations/ecommerce-product.d.ts +9 -1
  278. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  279. package/dist/esm/transformations/ecommerce-product.js +29 -2
  280. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  281. package/dist/esm/transformations/heading-anchors.d.ts +25 -0
  282. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -0
  283. package/dist/esm/transformations/heading-anchors.js +25 -0
  284. package/dist/esm/transformations/heading-anchors.js.map +1 -0
  285. package/dist/esm/transformations/index.d.ts +61 -4
  286. package/dist/esm/transformations/index.d.ts.map +1 -1
  287. package/dist/esm/transformations/index.js +69 -19
  288. package/dist/esm/transformations/index.js.map +1 -1
  289. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  290. package/dist/esm/transformations/shared-utils.js +1 -1
  291. package/dist/esm/transformations/shared-utils.js.map +1 -1
  292. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  293. package/dist/esm/utils.d.ts +11 -1
  294. package/dist/esm/utils.d.ts.map +1 -1
  295. package/dist/esm/utils.js +60 -16
  296. package/dist/esm/utils.js.map +1 -1
  297. package/package.json +5 -5
  298. package/src/array-overlap-sql.ts +41 -0
  299. package/src/content-localization.ts +53 -0
  300. package/src/count-effect.ts +53 -2
  301. package/src/data-source-fetchers.ts +41 -5
  302. package/src/fetchers/airtable.ts +4 -2
  303. package/src/fetchers/clickhouse.ts +4 -2
  304. package/src/fetchers/firestore.ts +4 -2
  305. package/src/fetchers/javascript.ts +9 -1
  306. package/src/fetchers/mariadb.ts +4 -2
  307. package/src/fetchers/mongodb.ts +4 -2
  308. package/src/fetchers/mysql.ts +4 -2
  309. package/src/fetchers/postgresql.ts +53 -12
  310. package/src/fetchers/raw-query.ts +60 -5
  311. package/src/fetchers/redshift.ts +4 -2
  312. package/src/fetchers/rest-api.ts +27 -5
  313. package/src/fetchers/teleport.ts +85 -22
  314. package/src/fetchers/turso.ts +4 -2
  315. package/src/fetchers/utils/browser-row-policy.ts +212 -0
  316. package/src/fetchers/utils/request-sql-guards.ts +25 -0
  317. package/src/fetchers/utils/rest-api-response-cache.ts +188 -0
  318. package/src/fetchers/utils/table-access-guard.ts +39 -0
  319. package/src/index.ts +79 -11
  320. package/src/items-path.ts +74 -0
  321. package/src/loading-state.ts +134 -3
  322. package/src/localized-columns.ts +146 -0
  323. package/src/pagination-plugin.ts +206 -60
  324. package/src/product-filter-fields.ts +173 -0
  325. package/src/request-locale.ts +113 -0
  326. package/src/search-clear.ts +102 -0
  327. package/src/transformations/blog-adjacent-posts.ts +74 -0
  328. package/src/transformations/blog-comments.ts +132 -0
  329. package/src/transformations/blog-post.ts +38 -5
  330. package/src/transformations/ecommerce-product.ts +568 -5
  331. package/src/transformations/heading-anchors.ts +85 -0
  332. package/src/transformations/index.ts +191 -23
  333. package/src/transformations/shared-utils.ts +6 -0
  334. package/src/utils.ts +109 -19
@@ -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'
@@ -7,6 +11,12 @@ import {
7
11
  generateEcommerceProductTransformationCode,
8
12
  type EcommerceProductTransformOptions,
9
13
  } from './ecommerce-product'
14
+ import { REQUEST_LOCALE_PARAM } from '../request-locale'
15
+ import {
16
+ resolveContentLocalization,
17
+ type ContentLocalization,
18
+ type LocalizedProjectOptions,
19
+ } from '../content-localization'
10
20
 
11
21
  export type { EcommerceProductTransformOptions }
12
22
 
@@ -19,6 +29,27 @@ export type { EcommerceProductTransformOptions }
19
29
  export interface EntityTransformOptions extends EcommerceProductTransformOptions {
20
30
  /** Blog post-category taxonomy — see `blogSettings.categories`. */
21
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
40
+ /**
41
+ * The languages the rows are stored in — absent for a single-language
42
+ * project. The transform resolves every translatable field against the
43
+ * request's locale (`?locale=es` → `es_name`, falling back to `name`), and
44
+ * the fetcher matches and sorts translatable columns the same way.
45
+ */
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[]
22
53
  }
23
54
 
24
55
  /**
@@ -47,12 +78,32 @@ const resolveAllowBackorders = (
47
78
  * "stock never blocks a purchase" flag — see `resolveAllowBackorders`.
48
79
  */
49
80
  export const buildProductTransformOptions = (
50
- options: Pick<GeneratorOptions, 'ecommerceSettings' | 'invoiceSettings' | 'blogSettings'>
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
+ }
51
97
  ): EntityTransformOptions => ({
52
98
  categories: options.ecommerceSettings?.categories,
53
99
  blogCategories: options.blogSettings?.categories,
100
+ blogHeadingAnchors: options.blogSettings?.headingAnchors === true,
101
+ blogComments: options.blogSettings?.comments === true,
54
102
  storefrontTaxRate: StorefrontTax.resolveStorefrontTaxRate(options.invoiceSettings),
55
103
  allowBackorders: resolveAllowBackorders(options.ecommerceSettings),
104
+ productPages: options.ecommerceSettings?.productPages,
105
+ localization: resolveContentLocalization(options),
106
+ trustedReaderRoles: TableAccess.resolveTrustedReaderRoles(options.auth || options.authentication),
56
107
  })
57
108
 
58
109
  export type TransformationType = 'blog-post' | 'ecommerce-product' | 'custom-page' | null
@@ -117,7 +168,13 @@ export const getTransformationCode = (
117
168
 
118
169
  switch (type) {
119
170
  case 'blog-post':
120
- return shared + generateBlogPostTransformationCode({ categories: options.blogCategories })
171
+ return (
172
+ shared +
173
+ generateBlogPostTransformationCode({
174
+ categories: options.blogCategories,
175
+ headingAnchors: options.blogHeadingAnchors,
176
+ })
177
+ )
121
178
  case 'ecommerce-product':
122
179
  return shared + generateEcommerceProductTransformationCode(options)
123
180
  case 'custom-page':
@@ -127,30 +184,99 @@ export const getTransformationCode = (
127
184
  }
128
185
  }
129
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
+
130
205
  /**
131
206
  * Returns the JavaScript expression that transforms the data array.
132
207
  * This expression assumes the data array variable is called `safeData`,
133
208
  * that `getClient` is available for asset resolution, and that `req` is in scope.
134
- * 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`).
135
211
  */
136
212
  export const getTransformExpression = (tableName: string): string | null => {
137
- const type = detectTransformationType(tableName)
138
- if (!type) {
213
+ if (!detectTransformationType(tableName)) {
139
214
  return null
140
215
  }
216
+ return '(__tqWantsRawRows(req.query) ? await rawRecords(safeData, getClient) : await transformRecords(safeData, getClient, req.query))'
217
+ }
141
218
 
142
- switch (type) {
143
- case 'blog-post':
144
- return 'await transformRecords(safeData, getClient, req.query)'
145
- case 'ecommerce-product':
146
- return 'await transformRecords(safeData, getClient, req.query)'
147
- case 'custom-page':
148
- return 'await transformRecords(safeData, getClient, req.query)'
149
- default:
150
- 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)
151
250
  }
251
+ return out
252
+ }
253
+
254
+ function __tqIsAssetId(value, assetMap) {
255
+ return typeof value === 'string' && value !== '' && Object.prototype.hasOwnProperty.call(assetMap, value)
152
256
  }
153
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
+
154
280
  /**
155
281
  * How many reviews a product page carries into its structured data.
156
282
  *
@@ -165,13 +291,24 @@ export const REVIEWS_PER_PRODUCT = 5
165
291
  * Returns the transform wrapper function code that handles asset map loading
166
292
  * and calls the appropriate transformer.
167
293
  * Returns empty string if no transformation is needed.
294
+ *
295
+ * The request's `locale` (see `REQUEST_LOCALE_PARAM`) is what every
296
+ * translatable field resolves against; the MAIN locale it falls back to is a
297
+ * project constant and is baked in from `options.localization` rather than
298
+ * asked of every caller. Without a localization every row reads as before —
299
+ * the base columns.
168
300
  */
169
- export const getTransformWrapperCode = (tableName: string): string => {
301
+ export const getTransformWrapperCode = (
302
+ tableName: string,
303
+ options: Pick<EntityTransformOptions, 'localization' | 'blogComments'> = {}
304
+ ): string => {
170
305
  const type = detectTransformationType(tableName)
171
306
  if (!type) {
172
307
  return ''
173
308
  }
174
309
 
310
+ const mainLanguageLiteral = JSON.stringify(options.localization?.mainLocale ?? null)
311
+
175
312
  const transformFn =
176
313
  type === 'blog-post'
177
314
  ? 'transformBlogPosts'
@@ -247,9 +384,11 @@ export const getTransformWrapperCode = (tableName: string): string => {
247
384
  }`
248
385
  : ''
249
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.
250
389
  const variantOption =
251
390
  type === 'ecommerce-product'
252
- ? ', variantsByProductId: variantsByProductId, ratingsByProductId: ratingsByProductId, reviewsByProductId: reviewsByProductId'
391
+ ? ', variantsByProductId: variantsByProductId, ratingsByProductId: ratingsByProductId, reviewsByProductId: reviewsByProductId, includeOptionDetail: Array.isArray(records) && records.length === 1'
253
392
  : ''
254
393
 
255
394
  // Related items are resolved for a SINGLE-record fetch only — which is the
@@ -281,17 +420,46 @@ export const getTransformWrapperCode = (tableName: string): string => {
281
420
  : ''
282
421
  const relatedOption = hasRelatedItems ? `, ${relatedMapVar}: ${relatedMapVar}` : ''
283
422
 
284
- 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}
285
454
  async function transformRecords(records, getClientFn, reqQuery) {
286
455
  var assetMap = {}
287
456
  try {
288
457
  assetMap = await getAssetMap(getClientFn)
289
458
  } catch (e) {
290
459
  // Asset resolution is best-effort; continue without it
291
- }${relatedEnrichment}${variantEnrichment}
292
- var currentLanguage = (reqQuery && reqQuery.lang) || null
293
- var mainLanguage = (reqQuery && reqQuery.mainLang) || null
294
- var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: mainLanguage${variantOption}${relatedOption} }
460
+ }${relatedEnrichment}${blogPageEnrichment}${variantEnrichment}
461
+ var currentLanguage = reqQuery && typeof reqQuery.${REQUEST_LOCALE_PARAM} === 'string' && reqQuery.${REQUEST_LOCALE_PARAM} ? reqQuery.${REQUEST_LOCALE_PARAM} : null
462
+ var options = { assetMap: assetMap, currentLanguage: currentLanguage, mainLanguage: ${mainLanguageLiteral}${variantOption}${relatedOption}${blogPageOption} }
295
463
  return ${transformFn}(records, options)
296
464
  }
297
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
@@ -14,6 +14,7 @@ import { GenericUtils, StringUtils } from '@teleporthq/teleport-shared'
14
14
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
15
15
  import type { EntityTransformOptions } from './transformations'
16
16
  import { DATA_SOURCE_ISR_REVALIDATE_SECONDS } from './isr'
17
+ import { buildServerLocaleParam, hasLocaleParam, buildClientLocaleParam } from './request-locale'
17
18
 
18
19
  const VALID_DATA_SOURCE_TYPES: DataSourceType[] = [
19
20
  'rest-api',
@@ -187,18 +188,28 @@ export const generateSafeFileName = (
187
188
  return StringUtils.camelCaseToDashCase(baseName)
188
189
  }
189
190
 
191
+ /**
192
+ * Wires a `DataProvider` to a client-side fetch of its API route.
193
+ *
194
+ * In a localized project (`transformOptions.localization`) the fetch carries
195
+ * the visitor's locale: it is added to the provider's `params` — created when
196
+ * the node declares none — so it reaches the route AND keys the client cache.
197
+ * The caller puts `router` in scope when this answers `true`.
198
+ *
199
+ * @returns `true` when a provider was wired to a client fetch.
200
+ */
190
201
  export const extractDataSourceIntoNextAPIFolder = (
191
202
  node: UIDLDataSourceItemNode | UIDLDataSourceListNode,
192
203
  dataSources: Record<string, UIDLDataSource>,
193
204
  componentChunk: ChunkDefinition,
194
205
  extractedResources: GeneratorOptions['extractedResources'],
195
206
  transformOptions: EntityTransformOptions = {}
196
- ) => {
207
+ ): boolean => {
197
208
  try {
198
209
  // Validate node content structure
199
210
  const contentValidation = validateNodeContent(node.content)
200
211
  if (!contentValidation.isValid) {
201
- return
212
+ return false
202
213
  }
203
214
 
204
215
  const { resourceDefinition } = node.content
@@ -206,31 +217,31 @@ export const extractDataSourceIntoNextAPIFolder = (
206
217
  // Validate resource definition
207
218
  const resourceValidation = validateResourceDefinition(resourceDefinition)
208
219
  if (!resourceValidation.isValid) {
209
- return
220
+ return false
210
221
  }
211
222
 
212
223
  const { dataSourceId, tableName, dataSourceType } = resourceDefinition
213
224
 
214
225
  // Check if dataSources object exists
215
226
  if (!dataSources || typeof dataSources !== 'object') {
216
- return
227
+ return false
217
228
  }
218
229
 
219
230
  // Check if data source exists
220
231
  const dataSource = dataSources[dataSourceId]
221
232
  if (!dataSource) {
222
- return
233
+ return false
223
234
  }
224
235
 
225
236
  // Validate data source configuration
226
237
  const configValidation = validateDataSourceConfig(dataSource)
227
238
  if (!configValidation.isValid) {
228
- return
239
+ return false
229
240
  }
230
241
 
231
242
  // Check if component chunk has meta and nodesLookup
232
243
  if (!componentChunk.meta || !componentChunk.meta.nodesLookup) {
233
- return
244
+ return false
234
245
  }
235
246
 
236
247
  // Generate safe file name
@@ -238,7 +249,7 @@ export const extractDataSourceIntoNextAPIFolder = (
238
249
 
239
250
  // Check if file name is valid
240
251
  if (!fileName || fileName === 'unknown') {
241
- return
252
+ return false
242
253
  }
243
254
 
244
255
  // Find JSX node by searching through nodesLookup
@@ -322,12 +333,12 @@ export const extractDataSourceIntoNextAPIFolder = (
322
333
  }
323
334
 
324
335
  if (!jsxNode || jsxNode.type !== 'JSXElement') {
325
- return
336
+ return false
326
337
  }
327
338
 
328
339
  // Ensure opening element and attributes exist
329
340
  if (!jsxNode.openingElement || !Array.isArray(jsxNode.openingElement.attributes)) {
330
- return
341
+ return false
331
342
  }
332
343
 
333
344
  // Check if this node has already been processed (has fetchData attribute)
@@ -336,12 +347,16 @@ export const extractDataSourceIntoNextAPIFolder = (
336
347
  )
337
348
 
338
349
  if (existingFetchData) {
339
- return
350
+ return false
340
351
  }
341
352
 
342
353
  // Check if there are resource params
343
354
  const resourceParams = node.content.resource?.params || {}
344
- const hasParams = Object.keys(resourceParams).length > 0
355
+ const localized = transformOptions.localization !== undefined
356
+ if (localized) {
357
+ addClientLocaleToParams(jsxNode)
358
+ }
359
+ const hasParams = localized || Object.keys(resourceParams).length > 0
345
360
 
346
361
  // Build resource path - use template literal if params exist
347
362
  let resourcePath: types.StringLiteral | types.TemplateLiteral
@@ -423,7 +438,7 @@ export const extractDataSourceIntoNextAPIFolder = (
423
438
 
424
439
  // Ensure extracted resources object exists
425
440
  if (!extractedResources || typeof extractedResources !== 'object') {
426
- return
441
+ return true
427
442
  }
428
443
 
429
444
  // Check if a utils file already exists for this data source (from getStaticProps)
@@ -440,7 +455,7 @@ export default dataSourceModule.handler
440
455
  path: ['pages', 'api'],
441
456
  content: apiRouteCode,
442
457
  }
443
- return
458
+ return true
444
459
  }
445
460
 
446
461
  // Generate fetcher code for API route (exports just the handler)
@@ -453,7 +468,7 @@ export default dataSourceModule.handler
453
468
  transformOptions
454
469
  )
455
470
  } catch (error) {
456
- return
471
+ return true
457
472
  }
458
473
 
459
474
  extractedResources[`api/${fileName}`] = {
@@ -462,11 +477,43 @@ export default dataSourceModule.handler
462
477
  path: ['pages', 'api'],
463
478
  content: fetcherCode,
464
479
  }
480
+ return true
465
481
  } catch (error) {
466
482
  // Catch any unexpected errors to prevent plugin from crashing
483
+ return false
467
484
  }
468
485
  }
469
486
 
487
+ /**
488
+ * Puts `locale: router?.locale` into a provider's `params` object literal,
489
+ * creating the attribute when the node declared no params at all.
490
+ */
491
+ const addClientLocaleToParams = (jsxNode: types.JSXElement): void => {
492
+ const attrs = jsxNode.openingElement.attributes
493
+ let paramsAttr = attrs.find(
494
+ (attr): attr is types.JSXAttribute =>
495
+ attr.type === 'JSXAttribute' &&
496
+ attr.name.type === 'JSXIdentifier' &&
497
+ attr.name.name === 'params'
498
+ )
499
+ if (!paramsAttr) {
500
+ paramsAttr = types.jsxAttribute(
501
+ types.jsxIdentifier('params'),
502
+ types.jsxExpressionContainer(types.objectExpression([]))
503
+ )
504
+ attrs.push(paramsAttr)
505
+ }
506
+ const value = paramsAttr.value
507
+ if (
508
+ value?.type !== 'JSXExpressionContainer' ||
509
+ value.expression.type !== 'ObjectExpression' ||
510
+ hasLocaleParam(value.expression.properties)
511
+ ) {
512
+ return
513
+ }
514
+ value.expression.properties.push(buildClientLocaleParam())
515
+ }
516
+
470
517
  export const isEmbeddedDataSource = (dataSourceType: string): boolean => {
471
518
  return ['javascript', 'csv-file'].includes(dataSourceType)
472
519
  }
@@ -581,8 +628,8 @@ export const generateSafeJSONParseCode = (): string => {
581
628
  * collapsing to an empty result the caller cannot explain.
582
629
  */
583
630
  export const generateCollectionPathHelperCode = (): string => {
584
- return `const resolveCollectionPath = (payload, rawPath) => {
585
- if (!rawPath) return payload
631
+ return `const parsePayloadPath = (rawPath) => {
632
+ if (!rawPath) return null
586
633
  let path = rawPath
587
634
  if (typeof path === 'string') {
588
635
  try {
@@ -591,17 +638,39 @@ export const generateCollectionPathHelperCode = (): string => {
591
638
  path = path.split('.').filter(Boolean)
592
639
  }
593
640
  }
594
- if (!Array.isArray(path) || path.length === 0) return payload
641
+ return Array.isArray(path) && path.length > 0 ? path : null
642
+ }
595
643
 
644
+ const readPayloadPath = (payload, path) => {
596
645
  let current = payload
597
646
  for (const segment of path) {
598
647
  if (!current || typeof current !== 'object' || Array.isArray(current) || !(segment in current)) {
599
- return payload
648
+ return undefined
600
649
  }
601
650
  current = current[segment]
602
651
  }
652
+ return current
653
+ }
603
654
 
655
+ const resolveCollectionPath = (payload, rawPath) => {
656
+ const path = parsePayloadPath(rawPath)
657
+ if (!path) return payload
658
+ const current = readPayloadPath(payload, path)
604
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) }
605
674
  }`
606
675
  }
607
676
 
@@ -766,6 +835,22 @@ const buildFilterTreeClause = (node, buildCondition) => {
766
835
  return buildCondition(node)
767
836
  }
768
837
 
838
+ // The generic multi-value operand. The editor sends the destination either as
839
+ // an array or as one comma-joined string (a URL-bound state such as
840
+ // "acme,globex"); both become an array, and "in" becomes "=" so every
841
+ // fetcher's existing array branch answers with IN (...). An empty set means
842
+ // "no filter", never "match nothing".
843
+ const normalizeInOperand = (operand, value) => {
844
+ if (operand !== 'in') return { operand, value }
845
+ const entries = Array.isArray(value) ? value : [value]
846
+ const values = entries
847
+ .flatMap((entry) => (typeof entry === 'string' ? entry.split(',') : [entry]))
848
+ .map((entry) => (typeof entry === 'string' ? entry.trim() : entry))
849
+ .filter((entry) => entry !== '' && entry !== null && entry !== undefined)
850
+ if (values.length === 0) return null
851
+ return { operand: '=', value: values }
852
+ }
853
+
769
854
  const flattenFilterConditions = (node) => {
770
855
  if (!node) return []
771
856
  if (node.type === 'group') {
@@ -1588,6 +1673,11 @@ export const extractDataSourceIntoGetStaticProps = (
1588
1673
  }
1589
1674
  })
1590
1675
 
1676
+ // getStaticProps runs once per language: the prefetch is for that language.
1677
+ if (transformOptions.localization !== undefined) {
1678
+ paramsProperties.push(buildServerLocaleParam())
1679
+ }
1680
+
1591
1681
  const fetchCallExpression = types.callExpression(
1592
1682
  types.memberExpression(types.identifier(fetcherImportName), types.identifier('fetchData')),
1593
1683
  [types.objectExpression(paramsProperties)]