@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
@@ -16,6 +16,12 @@ import {
16
16
  generateSortFieldSqlHelper,
17
17
  generateSortTiebreakSql,
18
18
  } from '../product-price-sort'
19
+ import { generateProductFilterClauseHelper } from '../product-filter-fields'
20
+ import { collectLocalizedColumns, generateLocalizedColumnsHelper } from '../localized-columns'
21
+ import { REQUEST_LOCALE_PARAM } from '../request-locale'
22
+ import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
23
+ import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
24
+ import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
19
25
 
20
26
  interface TeleportDBConfig {
21
27
  host?: string
@@ -27,6 +33,8 @@ interface TeleportDBConfig {
27
33
  ssl?: boolean | { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
28
34
  sslConfig?: { ca?: string; cert?: string; key?: string; rejectUnauthorized?: boolean }
29
35
  options?: { schema?: string }
36
+ /** The provisioned schema, as the editor recorded it — the source of the per-language columns. */
37
+ selectedTables?: Record<string, { columns?: Array<{ name?: string }> }>
30
38
  }
31
39
 
32
40
  const DEFAULT_ENV_KEYS = {
@@ -62,6 +70,10 @@ export const generateTeleportFetcher = (
62
70
  ): string => {
63
71
  const dbConfig = config as TeleportDBConfig
64
72
  const schema = dbConfig.options?.schema
73
+ const columnNames = (dbConfig.selectedTables?.[tableName]?.columns || [])
74
+ .map((column) => column.name)
75
+ .filter((name): name is string => typeof name === 'string')
76
+ const localizedColumns = collectLocalizedColumns(columnNames, transformOptions.localization)
65
77
 
66
78
  const hostRef = resolveEnvReference(dbConfig.host, DEFAULT_ENV_KEYS.host)
67
79
  const portRef = resolveEnvReference(dbConfig.port, DEFAULT_ENV_KEYS.port)
@@ -95,6 +107,7 @@ export const generateTeleportFetcher = (
95
107
 
96
108
  return `import { Client } from 'pg'
97
109
 
110
+ ${generateTableAccessPreamble(transformOptions.trustedReaderRoles || [])}
98
111
  function normalizePostgresConnectionString(connectionString) {
99
112
  if (!connectionString || typeof connectionString !== 'string') return connectionString;
100
113
  if (/^postgresql:\\/(?!\\/)/i.test(connectionString)) {
@@ -150,8 +163,10 @@ ${generateSafeJSONParseCode()}
150
163
  ${generateFilterTreeHelpersCode()}
151
164
 
152
165
  ${generateSearchEscapeHelpersCode()}
166
+ ${generateArrayOverlapSqlCode()}
153
167
  ${getTransformationCode(tableName, transformOptions)}
154
- ${getTransformWrapperCode(tableName)}
168
+ ${getTransformWrapperCode(tableName, transformOptions)}
169
+ ${generateRequestSqlGuardsCode()}
155
170
  const processFilters = (filters, conditions, queryParams, paramIndex) => {
156
171
  if (!filters) return paramIndex
157
172
 
@@ -160,16 +175,34 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
160
175
 
161
176
  const buildCondition = (condition) => {
162
177
  const field = condition.source
163
- const value = condition.destination
164
- const operand = condition.operand
178
+ const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)
179
+ if (normalizedCondition === null) return null
180
+ const value = normalizedCondition.value
181
+ const operand = normalizedCondition.operand
182
+
183
+ // The products table's virtual fields (effective price, rating bucket,
184
+ // on-sale flag, variant axes) are whole clauses, not columns - so they are
185
+ // answered before any branch below can interpolate the field name raw.
186
+ const productClause = productFilterClause(field, operand, value, (param) => {
187
+ queryParams.push(param)
188
+ return '$' + paramIndex++
189
+ })
190
+ if (productClause !== null) return productClause
191
+
192
+ // A translatable column is compared against the value in every language,
193
+ // so a localized slug resolves under any locale - see localized-columns.ts.
194
+ const localizedClause = localizedEqualityClause(field, operand, value, (param) => {
195
+ queryParams.push(param)
196
+ return '$' + paramIndex++
197
+ })
198
+ if (localizedClause !== null) return localizedClause
199
+ assertRequestColumn(field, 'filter field')
165
200
 
166
201
  if (Array.isArray(value)) {
167
202
  if (value.length === 0) return null
168
203
  if (operand === 'array_overlap') {
169
- // Row column is a JSON array (e.g. category_ids). Match when it
170
- // shares any element with the destination set. jsonb ?| (function
171
- // form) avoids the '?' placeholder-token ambiguity some drivers hit.
172
- const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
204
+ // The row's list (see array-overlap-sql.ts) shares any value with the set.
205
+ const clause = arrayOverlapSql((localizedColumnList(field) || [field]), '$' + paramIndex)
173
206
  queryParams.push(value.map((entry) => String(entry)))
174
207
  paramIndex++
175
208
  return clause
@@ -187,7 +220,7 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
187
220
  // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
188
221
  const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
189
222
  if (overlapValues.length === 0) return null
190
- const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
223
+ const clause = arrayOverlapSql((localizedColumnList(field) || [field]), '$' + paramIndex)
191
224
  queryParams.push(overlapValues)
192
225
  paramIndex++
193
226
  return clause
@@ -219,6 +252,10 @@ ${generateSortFieldSqlHelper(tableName)}
219
252
 
220
253
  ${generateSortFallbackFieldHelper(tableName)}
221
254
 
255
+ ${generateProductFilterClauseHelper(tableName)}
256
+
257
+ ${generateLocalizedColumnsHelper(localizedColumns)}
258
+
222
259
  // Matches DDL / dangerous statements the raw-query branch should refuse.
223
260
  // Keep this list conservative — anything destructive or schema-changing is
224
261
  // out of scope for a client-triggered fetch. SELECT and CTEs (WITH ...) are
@@ -263,6 +300,7 @@ function assertRawQuerySafe(rawQuery) {
263
300
  }
264
301
 
265
302
  export default async function handler(req, res) {
303
+ ${generateReadGuardCall(tableName, 'req.query && req.query.rawQuery')}
266
304
  const client = getClient()
267
305
 
268
306
  try {
@@ -285,12 +323,18 @@ export default async function handler(req, res) {
285
323
  const rawSafe = JSON.parse(JSON.stringify(rawPlain, dateReplacer))
286
324
  return res.status(200).json({
287
325
  success: true,
288
- data: rawSafe,
326
+ // Every row here ends up in a browser (a page's props or a client
327
+ // refetch), so no credential column survives, whatever joined it.
328
+ data: __taWithoutCredentials(rawSafe, null),
289
329
  timestamp: Date.now()
290
330
  })
291
331
  }
292
332
 
293
333
  const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
334
+ // A visitor never filters, sorts or searches by a column it is not served.
335
+ if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
336
+ // The language the page is in - what a translatable sort orders by.
337
+ const requestLocale = typeof req.query.${REQUEST_LOCALE_PARAM} === 'string' ? req.query.${REQUEST_LOCALE_PARAM} : null
294
338
 
295
339
  const conditions = []
296
340
  const queryParams = []
@@ -323,6 +367,7 @@ export default async function handler(req, res) {
323
367
  console.warn('Failed to fetch column names from information_schema:', schemaError.message)
324
368
  }
325
369
  }
370
+ if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
326
371
 
327
372
  if (columns.length > 0) {
328
373
  const pattern = '%' + escapeLikePattern(query) + '%'
@@ -337,6 +382,7 @@ export default async function handler(req, res) {
337
382
  }
338
383
 
339
384
  paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
385
+ if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
340
386
 
341
387
  let sql = \`SELECT * FROM ${tableName}\`
342
388
 
@@ -345,15 +391,18 @@ export default async function handler(req, res) {
345
391
  }
346
392
 
347
393
  // Handle sorts - new array format. Two ORDER BY clauses are built: the one
348
- // actually used, and a plain-column twin kept only as the fallback below.
394
+ // actually used (a translatable column in the request's language, the
395
+ // products table's price rewrites), and a plain-column twin kept only as
396
+ // the fallback below.
349
397
  let orderBySql = ''
350
398
  let plainOrderBySql = ''
351
399
  if (sorts) {
352
400
  const parsedSorts = safeJSONParse(sorts)
353
401
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
354
402
  const valid = parsedSorts.filter((sort) => sort && sort.field)
403
+ valid.forEach((sort) => assertRequestColumn(sort.field, 'sort field'))
355
404
  const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
356
- const orderClauses = valid.map((sort) => \`\${sortFieldSql(sort.field)} \${orderOf(sort)}\`)
405
+ const orderClauses = valid.map((sort) => \`\${localizedSortFieldSql(sort.field, requestLocale) || sortFieldSql(sort.field)} \${orderOf(sort)}\`)
357
406
  const plainClauses = valid.map((sort) => \`\${sortFallbackField(sort.field)} \${orderOf(sort)}\`)
358
407
 
359
408
  if (orderClauses.length > 0) {
@@ -369,16 +418,18 @@ export default async function handler(req, res) {
369
418
  }
370
419
  }
371
420
  } else if (sortBy) {
372
- orderBySql = \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
373
- plainOrderBySql = orderBySql
421
+ assertRequestColumn(sortBy, 'sort field')
422
+ const sortByDirection = (sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
423
+ orderBySql = \` ORDER BY \${localizedSortFieldSql(sortBy, requestLocale) || sortBy} \${sortByDirection}\`
424
+ plainOrderBySql = \` ORDER BY \${sortBy} \${sortByDirection}\`
374
425
  }
375
426
  const usedDiscountAwareSort = orderBySql !== plainOrderBySql
376
427
 
377
- const limitValue = limit || perPage
428
+ const limitValue = requestRowCount(limit || perPage)
378
429
  const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
379
430
 
380
431
  let sqlTail = ''
381
- if (limitValue) {
432
+ if (limitValue !== undefined) {
382
433
  sqlTail += \` LIMIT \${limitValue}\`
383
434
  }
384
435
 
@@ -392,14 +443,16 @@ export default async function handler(req, res) {
392
443
  // The discount-aware price ordering is an inline sub-select over a JSON
393
444
  // column. It is written to be unraisable, but it runs inside ORDER BY for
394
445
  // the whole table - so if a database ever rejects it, fall back to ordering
395
- // by the stored list price rather than serving an empty products page.
446
+ // by the stored list price rather than serving an empty products page. The
447
+ // translatable-column ordering takes the same exit: a per-language column
448
+ // the schema had at generation time may have been dropped since.
396
449
  let result
397
450
  try {
398
451
  result = await client.query(sql, queryParams)
399
452
  } catch (sortError) {
400
453
  if (!usedDiscountAwareSort) throw sortError
401
454
  console.warn(
402
- 'Discount-aware price sort failed; falling back to the list price:',
455
+ 'Rewritten sort failed; falling back to the plain column:',
403
456
  sortError && sortError.message
404
457
  )
405
458
  result = await client.query(baseSql + plainOrderBySql + sqlTail, queryParams)
@@ -430,14 +483,20 @@ export default async function handler(req, res) {
430
483
 
431
484
  return res.status(200).json({
432
485
  success: true,
433
- data: __numberedData,
486
+ data: __taWithoutCredentials(
487
+ __rowPolicy ? __brpStripHidden(__rowPolicy, __numberedData) : __numberedData,
488
+ ${JSON.stringify(tableName)}
489
+ ),
434
490
  timestamp: Date.now()
435
491
  })
436
492
  } catch (error) {
437
493
  console.error('Teleport DB fetch error:', error)
438
- return res.status(500).json({
494
+ // A database error names tables, columns and values: it stays in the server
495
+ // log. Only an error this route raised for the caller (it carries a status)
496
+ // is repeated to it.
497
+ return res.status(error.status || 500).json({
439
498
  success: false,
440
- error: error.message || 'Failed to fetch data',
499
+ error: error.status ? error.message : 'Failed to fetch data',
441
500
  timestamp: Date.now()
442
501
  })
443
502
  } finally {
@@ -462,11 +521,13 @@ export const generateTeleportCountFetcher = (
462
521
 
463
522
  return `
464
523
  async function getCount(req, res) {
524
+ ${generateReadGuardCall(tableName, null)}
465
525
  const client = getClient()
466
526
 
467
527
  try {
468
528
  await client.connect()
469
529
  const { query, queryColumns, filters } = req.query
530
+ if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
470
531
  const conditions = []
471
532
  const queryParams = []
472
533
  let paramIndex = 1
@@ -498,6 +559,7 @@ async function getCount(req, res) {
498
559
  console.warn('Failed to fetch column names from information_schema:', schemaError.message)
499
560
  }
500
561
  }
562
+ if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
501
563
 
502
564
  if (columns.length > 0) {
503
565
  const pattern = '%' + escapeLikePattern(query) + '%'
@@ -514,6 +576,7 @@ async function getCount(req, res) {
514
576
  }
515
577
 
516
578
  paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
579
+ if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
517
580
 
518
581
  let countSql = \`SELECT COUNT(*) FROM ${tableName}\`
519
582
  if (conditions.length > 0) {
@@ -530,9 +593,9 @@ async function getCount(req, res) {
530
593
  })
531
594
  } catch (error) {
532
595
  console.error('Error getting count:', error)
533
- return res.status(500).json({
596
+ return res.status(error.status || 500).json({
534
597
  success: false,
535
- error: error.message || 'Failed to get count',
598
+ error: error.status ? error.message : 'Failed to get count',
536
599
  timestamp: Date.now()
537
600
  })
538
601
  } finally {
@@ -100,8 +100,10 @@ export default async function handler(req, res) {
100
100
  if (filterTree) {
101
101
  const buildCondition = (condition) => {
102
102
  const field = sanitizeIdentifier(condition.source)
103
- const value = condition.destination
104
- const operand = condition.operand
103
+ const normalizedCondition = normalizeInOperand(condition.operand, condition.destination)
104
+ if (normalizedCondition === null) return null
105
+ const value = normalizedCondition.value
106
+ const operand = normalizedCondition.operand
105
107
 
106
108
  if (Array.isArray(value)) {
107
109
  if (value.length === 0) return null
@@ -0,0 +1,212 @@
1
+ /**
2
+ * What a per-table read route (`/api/teleport-<table>-<source>` and its count
3
+ * twin) serves a BROWSER of the tables whose unpublished rows sit beside the
4
+ * public ones. (The tables it serves a browser not at all — the AI assistant's
5
+ * among them — are `TableAccess.PROTECTED_TABLES`, guarded by `__taGuardRead`.)
6
+ *
7
+ * The route answers whoever asks, with the whole table. For the blog that was
8
+ * every draft, scheduled and archived post — and each author's email address;
9
+ * for the catalogue, every draft and inactive product. The storefront's own
10
+ * bindings ask for the public rows, but the route is a URL anyone can call
11
+ * without them. So a caller that is not the store itself (its server code, or a
12
+ * signed-in administrator — the generated admin edits drafts through these
13
+ * routes) is served only the rows the storefront publishes, never a hidden
14
+ * column, and may neither filter, sort nor search by a hidden column: a filter
15
+ * is an oracle for a column it never returns.
16
+ */
17
+
18
+ export interface BrowserRowPolicy {
19
+ /** SQL every browser read of the table is narrowed with. */
20
+ predicate: string
21
+ /** Keys never served to a browser, wherever they sit in a row (raw and transformed names). */
22
+ hiddenColumns: ReadonlyArray<string>
23
+ }
24
+
25
+ export const BROWSER_ROW_POLICIES: Readonly<Record<string, BrowserRowPolicy>> = {
26
+ // ONLY `published` is public — the blog's own rule (see the GUI's
27
+ // `blog-post-status.ts`): drafts, scheduled and archived posts are not.
28
+ teleport_blog_posts: {
29
+ predicate: "status = 'published'",
30
+ hiddenColumns: ['author_email', 'authorEmail'],
31
+ },
32
+ // The storefront sells `active` products; a draft or inactive one is the
33
+ // merchant's, spelled in any case the admin form stored it in.
34
+ teleport_products: {
35
+ predicate: "LOWER(TRIM(status)) = 'active'",
36
+ hiddenColumns: [],
37
+ },
38
+ }
39
+
40
+ /**
41
+ * ES5 source inlined after the table-access preamble (it uses `__ta*` and
42
+ * `__tqSessionToken`). Declares `__brpAccess(req, tableName, rawQuery)`, which
43
+ * resolves to `{ denied, policy }`: `denied` a `{ status, message }` refusal or
44
+ * null, `policy` the narrowing to apply to this read or null (the caller reads
45
+ * the table whole). Plus the helpers that apply a policy to a request and to
46
+ * its rows.
47
+ */
48
+ export const generateBrowserRowPolicyCode =
49
+ (): string => `// GENERATED — see browser-row-policy.ts in teleport-plugin-next-data-source.
50
+ var __BRP_POLICIES = ${JSON.stringify(BROWSER_ROW_POLICIES)};
51
+
52
+ // Who is asking: the store's own server code and its administrators read every
53
+ // row; anyone else is a visitor.
54
+ async function __brpReader(req) {
55
+ if (__taIsInternalRequest(req)) return { trusted: true };
56
+ var token = null;
57
+ try {
58
+ token = await __tqSessionToken(req);
59
+ } catch (e) {
60
+ token = null;
61
+ }
62
+ var role = token ? __taRoleOf(token) : null;
63
+ return { trusted: !!role && __TA_TRUSTED_READER_ROLES.indexOf(role) !== -1 };
64
+ }
65
+
66
+ function __brpMentionsAny(rawQuery, tables) {
67
+ for (var i = 0; i < tables.length; i++) {
68
+ if (__taSqlMentionsTable(rawQuery, tables[i])) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // A raw statement cannot be narrowed, so a visitor's statement that names a
74
+ // narrowed table is refused whole. The session is only decoded when the read
75
+ // touches one of these tables.
76
+ async function __brpAccess(req, tableName, rawQuery) {
77
+ var raw = typeof rawQuery === 'string' && rawQuery.length > 0 ? rawQuery : null;
78
+ var bare = __taNormalizeTable(tableName);
79
+ var rawNarrowed = !!raw && __brpMentionsAny(raw, Object.keys(__BRP_POLICIES));
80
+ var policy = raw ? null : (Object.prototype.hasOwnProperty.call(__BRP_POLICIES, bare) ? __BRP_POLICIES[bare] : null);
81
+ if (!rawNarrowed && !policy) return { denied: null, policy: null };
82
+ var reader = await __brpReader(req);
83
+ if (reader.trusted) return { denied: null, policy: null };
84
+ if (rawNarrowed) {
85
+ return { denied: { status: 403, message: 'Forbidden: raw queries over this table are only available to server-side calls' }, policy: null };
86
+ }
87
+ return { denied: null, policy: policy };
88
+ }
89
+
90
+ function __brpColumnName(field) {
91
+ var parts = String(field == null ? '' : field).replace(/"/g, '').trim().split('.');
92
+ return parts[parts.length - 1];
93
+ }
94
+
95
+ function __brpIsHidden(policy, field) {
96
+ var name = __brpColumnName(field).toLowerCase();
97
+ for (var i = 0; i < policy.hiddenColumns.length; i++) {
98
+ if (policy.hiddenColumns[i].toLowerCase() === name) return true;
99
+ }
100
+ return false;
101
+ }
102
+
103
+ // Every field a request filters, sorts or searches by, however deep a filter
104
+ // group nests it. A condition's value is data, not a field.
105
+ function __brpRequestFields(value, out) {
106
+ if (Array.isArray(value)) {
107
+ for (var i = 0; i < value.length; i++) __brpRequestFields(value[i], out);
108
+ } else if (value && typeof value === 'object') {
109
+ if (typeof value.source === 'string') out.push(value.source);
110
+ if (typeof value.field === 'string') out.push(value.field);
111
+ var keys = Object.keys(value);
112
+ for (var k = 0; k < keys.length; k++) {
113
+ if (keys[k] === 'destination' || keys[k] === 'value') continue;
114
+ if (value[keys[k]] && typeof value[keys[k]] === 'object') __brpRequestFields(value[keys[k]], out);
115
+ }
116
+ } else if (typeof value === 'string' && value) {
117
+ out.push(value);
118
+ }
119
+ return out;
120
+ }
121
+
122
+ function __brpParse(value) {
123
+ if (typeof value !== 'string') return value;
124
+ try {
125
+ return JSON.parse(value);
126
+ } catch (e) {
127
+ return value;
128
+ }
129
+ }
130
+
131
+ function __brpAssertVisibleFields(policy, query) {
132
+ var q = query || {};
133
+ var fields = [];
134
+ __brpRequestFields(__brpParse(q.filters), fields);
135
+ __brpRequestFields(__brpParse(q.sorts), fields);
136
+ __brpRequestFields(__brpParse(q.queryColumns), fields);
137
+ if (typeof q.sortBy === 'string') fields.push(q.sortBy);
138
+ for (var i = 0; i < fields.length; i++) {
139
+ if (__brpIsHidden(policy, fields[i])) {
140
+ var err = new Error('Forbidden: ' + __brpColumnName(fields[i]) + ' is not readable');
141
+ err.status = 403;
142
+ throw err;
143
+ }
144
+ }
145
+ }
146
+
147
+ function __brpVisibleColumns(policy, columns) {
148
+ return columns.filter(function(column) { return !__brpIsHidden(policy, column); });
149
+ }
150
+
151
+ // The rows without a hidden key, at any depth: a transformed row nests related
152
+ // rows (a post's related and adjacent posts) built the same way.
153
+ function __brpStripHidden(policy, value) {
154
+ if (Array.isArray(value)) {
155
+ return value.map(function(entry) { return __brpStripHidden(policy, entry); });
156
+ }
157
+ if (!value || typeof value !== 'object') return value;
158
+ var copy = {};
159
+ Object.keys(value).forEach(function(key) {
160
+ if (!__brpIsHidden(policy, key)) copy[key] = __brpStripHidden(policy, value[key]);
161
+ });
162
+ return copy;
163
+ }
164
+
165
+ // Whether this request is answered exactly as an anonymous visitor's would be,
166
+ // so a response cached for one may serve the other. A read whose answer depends
167
+ // on who asks (\`viewDependent\`: a narrowed, protected or admin-only table) or
168
+ // that runs a raw statement differs for the store's own server code and its
169
+ // administrators, and must never share a cache entry with a visitor.
170
+ async function __brpSharesPublicView(req, viewDependent) {
171
+ var raw = !!(req && req.query && typeof req.query.rawQuery === 'string' && req.query.rawQuery.length > 0);
172
+ if (!viewDependent && !raw) return true;
173
+ return !(await __brpReader(req)).trusted;
174
+ }
175
+ `
176
+
177
+ /**
178
+ * Whether a per-table read route's answer depends on who asks — the rows it
179
+ * narrows, the tables it guards — so its response cache must be split by view.
180
+ */
181
+ export const isViewDependentTable = (
182
+ tableName: string,
183
+ protectedTables: ReadonlyArray<string>
184
+ ): boolean => {
185
+ const parts = String(tableName || '')
186
+ .trim()
187
+ .toLowerCase()
188
+ .split('.')
189
+ const bare = parts[parts.length - 1].replace(/"/g, '')
190
+ return (
191
+ Object.prototype.hasOwnProperty.call(BROWSER_ROW_POLICIES, bare) ||
192
+ protectedTables.indexOf(bare) !== -1
193
+ )
194
+ }
195
+
196
+ /**
197
+ * The early return right after the table-access guard, plus the policy
198
+ * (`__rowPolicy`, null or a policy) the handler narrows the rest of its read by.
199
+ */
200
+ export const generateBrowserRowAccessCall = (
201
+ tableName: string,
202
+ rawQueryExpr: string | null
203
+ ): string => {
204
+ return ` const __rowAccess = await __brpAccess(req, ${JSON.stringify(tableName)}, ${
205
+ rawQueryExpr || 'null'
206
+ })
207
+ if (__rowAccess.denied) {
208
+ return res.status(__rowAccess.denied.status).json({ success: false, error: __rowAccess.denied.message, timestamp: Date.now() })
209
+ }
210
+ const __rowPolicy = __rowAccess.policy
211
+ `
212
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * What a per-table read route takes from its query string and splices into
3
+ * SQL: filter columns, sort columns and a row count. Values are bound as
4
+ * parameters; these three are not, so each is checked for the one shape it
5
+ * may have before it reaches the statement.
6
+ */
7
+ export const generateRequestSqlGuardsCode = (): string => `
8
+ // A column name as this route accepts one from a request: plain, optionally
9
+ // quoted or table-qualified. Anything else - an expression, a sub-select - is
10
+ // SQL the caller wrote, and it would run as part of this statement.
11
+ const REQUEST_COLUMN_RE = /^"?[A-Za-z_][A-Za-z0-9_]*"?(?:\\."?[A-Za-z_][A-Za-z0-9_]*"?)?$/
12
+ function assertRequestColumn(field, label) {
13
+ if (typeof field !== 'string' || !REQUEST_COLUMN_RE.test(field)) {
14
+ const error = new Error('Invalid ' + label)
15
+ error.status = 400
16
+ throw error
17
+ }
18
+ }
19
+
20
+ // A row count from a request, as a whole number or nothing.
21
+ function requestRowCount(value) {
22
+ const parsed = parseInt(value, 10)
23
+ return isNaN(parsed) || parsed < 0 ? undefined : parsed
24
+ }
25
+ `