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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/__tests__/array-overlap-sql.test.ts +53 -0
  2. package/__tests__/blog-post-page-enrichment.test.ts +214 -0
  3. package/__tests__/browser-row-policy.test.ts +503 -0
  4. package/__tests__/data-source-cache-wrapping.test.ts +18 -2
  5. package/__tests__/ecommerce-product-options.test.ts +206 -0
  6. package/__tests__/ecommerce-product-pages.test.ts +82 -0
  7. package/__tests__/ecommerce-product-pre-options.golden.json +505 -0
  8. package/__tests__/ecommerce-product-storefront-contract.test.ts +293 -0
  9. package/__tests__/ecommerce-product-subscriptions.test.ts +121 -0
  10. package/__tests__/fetchers.test.ts +2 -2
  11. package/__tests__/items-path.test.ts +150 -0
  12. package/__tests__/product-options-storefront.fixture.json +2510 -0
  13. package/__tests__/product-page-url-fixtures.json +25 -0
  14. package/__tests__/product-price-sort.ts +4 -2
  15. package/__tests__/raw-rows-mode.test.ts +111 -0
  16. package/__tests__/rest-api-response-cache.test.ts +434 -0
  17. package/__tests__/table-read-route-protected-tables.test.ts +493 -0
  18. package/dist/cjs/array-overlap-sql.d.ts +21 -0
  19. package/dist/cjs/array-overlap-sql.d.ts.map +1 -0
  20. package/dist/cjs/array-overlap-sql.js +25 -0
  21. package/dist/cjs/array-overlap-sql.js.map +1 -0
  22. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  23. package/dist/cjs/data-source-fetchers.js +10 -4
  24. package/dist/cjs/data-source-fetchers.js.map +1 -1
  25. package/dist/cjs/fetchers/javascript.d.ts.map +1 -1
  26. package/dist/cjs/fetchers/javascript.js +1 -1
  27. package/dist/cjs/fetchers/javascript.js.map +1 -1
  28. package/dist/cjs/fetchers/postgresql.d.ts +1 -1
  29. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  30. package/dist/cjs/fetchers/postgresql.js +8 -4
  31. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  32. package/dist/cjs/fetchers/raw-query.d.ts +8 -1
  33. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  34. package/dist/cjs/fetchers/raw-query.js +42 -4
  35. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  36. package/dist/cjs/fetchers/rest-api.d.ts.map +1 -1
  37. package/dist/cjs/fetchers/rest-api.js +7 -2
  38. package/dist/cjs/fetchers/rest-api.js.map +1 -1
  39. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  40. package/dist/cjs/fetchers/teleport.js +8 -5
  41. package/dist/cjs/fetchers/teleport.js.map +1 -1
  42. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +43 -0
  43. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  44. package/dist/cjs/fetchers/utils/browser-row-policy.js +66 -0
  45. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -0
  46. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts +8 -0
  47. package/dist/cjs/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  48. package/dist/cjs/fetchers/utils/request-sql-guards.js +12 -0
  49. package/dist/cjs/fetchers/utils/request-sql-guards.js.map +1 -0
  50. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  51. package/dist/cjs/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  52. package/dist/cjs/fetchers/utils/rest-api-response-cache.js +64 -0
  53. package/dist/cjs/fetchers/utils/rest-api-response-cache.js.map +1 -0
  54. package/dist/cjs/fetchers/utils/table-access-guard.d.ts +23 -0
  55. package/dist/cjs/fetchers/utils/table-access-guard.d.ts.map +1 -0
  56. package/dist/cjs/fetchers/utils/table-access-guard.js +34 -0
  57. package/dist/cjs/fetchers/utils/table-access-guard.js.map +1 -0
  58. package/dist/cjs/items-path.d.ts +10 -0
  59. package/dist/cjs/items-path.d.ts.map +1 -0
  60. package/dist/cjs/items-path.js +86 -0
  61. package/dist/cjs/items-path.js.map +1 -0
  62. package/dist/cjs/localized-columns.d.ts.map +1 -1
  63. package/dist/cjs/localized-columns.js +13 -0
  64. package/dist/cjs/localized-columns.js.map +1 -1
  65. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  66. package/dist/cjs/pagination-plugin.js +17 -5
  67. package/dist/cjs/pagination-plugin.js.map +1 -1
  68. package/dist/cjs/transformations/blog-adjacent-posts.d.ts +16 -0
  69. package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -0
  70. package/dist/cjs/transformations/blog-adjacent-posts.js +25 -0
  71. package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -0
  72. package/dist/cjs/transformations/blog-comments.d.ts +30 -0
  73. package/dist/cjs/transformations/blog-comments.d.ts.map +1 -0
  74. package/dist/cjs/transformations/blog-comments.js +34 -0
  75. package/dist/cjs/transformations/blog-comments.js.map +1 -0
  76. package/dist/cjs/transformations/blog-post.d.ts +2 -0
  77. package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
  78. package/dist/cjs/transformations/blog-post.js +4 -1
  79. package/dist/cjs/transformations/blog-post.js.map +1 -1
  80. package/dist/cjs/transformations/ecommerce-product.d.ts +9 -1
  81. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  82. package/dist/cjs/transformations/ecommerce-product.js +28 -1
  83. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  84. package/dist/cjs/transformations/heading-anchors.d.ts +25 -0
  85. package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -0
  86. package/dist/cjs/transformations/heading-anchors.js +29 -0
  87. package/dist/cjs/transformations/heading-anchors.js.map +1 -0
  88. package/dist/cjs/transformations/index.d.ts +47 -4
  89. package/dist/cjs/transformations/index.d.ts.map +1 -1
  90. package/dist/cjs/transformations/index.js +56 -18
  91. package/dist/cjs/transformations/index.js.map +1 -1
  92. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  93. package/dist/cjs/transformations/shared-utils.js +1 -1
  94. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  95. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  96. package/dist/cjs/utils.d.ts.map +1 -1
  97. package/dist/cjs/utils.js +1 -1
  98. package/dist/cjs/utils.js.map +1 -1
  99. package/dist/esm/array-overlap-sql.d.ts +21 -0
  100. package/dist/esm/array-overlap-sql.d.ts.map +1 -0
  101. package/dist/esm/array-overlap-sql.js +21 -0
  102. package/dist/esm/array-overlap-sql.js.map +1 -0
  103. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  104. package/dist/esm/data-source-fetchers.js +11 -5
  105. package/dist/esm/data-source-fetchers.js.map +1 -1
  106. package/dist/esm/fetchers/javascript.d.ts.map +1 -1
  107. package/dist/esm/fetchers/javascript.js +1 -1
  108. package/dist/esm/fetchers/javascript.js.map +1 -1
  109. package/dist/esm/fetchers/postgresql.d.ts +1 -1
  110. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  111. package/dist/esm/fetchers/postgresql.js +8 -4
  112. package/dist/esm/fetchers/postgresql.js.map +1 -1
  113. package/dist/esm/fetchers/raw-query.d.ts +8 -1
  114. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  115. package/dist/esm/fetchers/raw-query.js +42 -4
  116. package/dist/esm/fetchers/raw-query.js.map +1 -1
  117. package/dist/esm/fetchers/rest-api.d.ts.map +1 -1
  118. package/dist/esm/fetchers/rest-api.js +7 -2
  119. package/dist/esm/fetchers/rest-api.js.map +1 -1
  120. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  121. package/dist/esm/fetchers/teleport.js +8 -5
  122. package/dist/esm/fetchers/teleport.js.map +1 -1
  123. package/dist/esm/fetchers/utils/browser-row-policy.d.ts +43 -0
  124. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -0
  125. package/dist/esm/fetchers/utils/browser-row-policy.js +60 -0
  126. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -0
  127. package/dist/esm/fetchers/utils/request-sql-guards.d.ts +8 -0
  128. package/dist/esm/fetchers/utils/request-sql-guards.d.ts.map +1 -0
  129. package/dist/esm/fetchers/utils/request-sql-guards.js +8 -0
  130. package/dist/esm/fetchers/utils/request-sql-guards.js.map +1 -0
  131. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts +40 -0
  132. package/dist/esm/fetchers/utils/rest-api-response-cache.d.ts.map +1 -0
  133. package/dist/esm/fetchers/utils/rest-api-response-cache.js +59 -0
  134. package/dist/esm/fetchers/utils/rest-api-response-cache.js.map +1 -0
  135. package/dist/esm/fetchers/utils/table-access-guard.d.ts +23 -0
  136. package/dist/esm/fetchers/utils/table-access-guard.d.ts.map +1 -0
  137. package/dist/esm/fetchers/utils/table-access-guard.js +29 -0
  138. package/dist/esm/fetchers/utils/table-access-guard.js.map +1 -0
  139. package/dist/esm/items-path.d.ts +10 -0
  140. package/dist/esm/items-path.d.ts.map +1 -0
  141. package/dist/esm/items-path.js +58 -0
  142. package/dist/esm/items-path.js.map +1 -0
  143. package/dist/esm/localized-columns.d.ts.map +1 -1
  144. package/dist/esm/localized-columns.js +13 -0
  145. package/dist/esm/localized-columns.js.map +1 -1
  146. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  147. package/dist/esm/pagination-plugin.js +17 -5
  148. package/dist/esm/pagination-plugin.js.map +1 -1
  149. package/dist/esm/transformations/blog-adjacent-posts.d.ts +16 -0
  150. package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -0
  151. package/dist/esm/transformations/blog-adjacent-posts.js +21 -0
  152. package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -0
  153. package/dist/esm/transformations/blog-comments.d.ts +30 -0
  154. package/dist/esm/transformations/blog-comments.d.ts.map +1 -0
  155. package/dist/esm/transformations/blog-comments.js +30 -0
  156. package/dist/esm/transformations/blog-comments.js.map +1 -0
  157. package/dist/esm/transformations/blog-post.d.ts +2 -0
  158. package/dist/esm/transformations/blog-post.d.ts.map +1 -1
  159. package/dist/esm/transformations/blog-post.js +4 -1
  160. package/dist/esm/transformations/blog-post.js.map +1 -1
  161. package/dist/esm/transformations/ecommerce-product.d.ts +9 -1
  162. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  163. package/dist/esm/transformations/ecommerce-product.js +29 -2
  164. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  165. package/dist/esm/transformations/heading-anchors.d.ts +25 -0
  166. package/dist/esm/transformations/heading-anchors.d.ts.map +1 -0
  167. package/dist/esm/transformations/heading-anchors.js +25 -0
  168. package/dist/esm/transformations/heading-anchors.js.map +1 -0
  169. package/dist/esm/transformations/index.d.ts +47 -4
  170. package/dist/esm/transformations/index.d.ts.map +1 -1
  171. package/dist/esm/transformations/index.js +56 -18
  172. package/dist/esm/transformations/index.js.map +1 -1
  173. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  174. package/dist/esm/transformations/shared-utils.js +1 -1
  175. package/dist/esm/transformations/shared-utils.js.map +1 -1
  176. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  177. package/dist/esm/utils.d.ts.map +1 -1
  178. package/dist/esm/utils.js +1 -1
  179. package/dist/esm/utils.js.map +1 -1
  180. package/package.json +5 -5
  181. package/src/array-overlap-sql.ts +41 -0
  182. package/src/data-source-fetchers.ts +41 -5
  183. package/src/fetchers/javascript.ts +9 -1
  184. package/src/fetchers/postgresql.ts +37 -10
  185. package/src/fetchers/raw-query.ts +60 -5
  186. package/src/fetchers/rest-api.ts +27 -5
  187. package/src/fetchers/teleport.ts +37 -13
  188. package/src/fetchers/utils/browser-row-policy.ts +212 -0
  189. package/src/fetchers/utils/request-sql-guards.ts +25 -0
  190. package/src/fetchers/utils/rest-api-response-cache.ts +188 -0
  191. package/src/fetchers/utils/table-access-guard.ts +39 -0
  192. package/src/items-path.ts +74 -0
  193. package/src/localized-columns.ts +13 -0
  194. package/src/pagination-plugin.ts +29 -7
  195. package/src/transformations/blog-adjacent-posts.ts +74 -0
  196. package/src/transformations/blog-comments.ts +132 -0
  197. package/src/transformations/blog-post.ts +38 -5
  198. package/src/transformations/ecommerce-product.ts +568 -5
  199. package/src/transformations/heading-anchors.ts +85 -0
  200. package/src/transformations/index.ts +165 -22
  201. package/src/transformations/shared-utils.ts +6 -0
  202. package/src/utils.ts +26 -4
@@ -34,8 +34,9 @@ import {
34
34
  import { validateDatabaseConfig } from './validation'
35
35
  import { generateCountFetcher } from './count-fetchers'
36
36
  import type { EntityTransformOptions } from './transformations'
37
- import { DataCache } from '@teleporthq/teleport-shared'
37
+ import { DataCache, TableAccess } from '@teleporthq/teleport-shared'
38
38
  import type { DataSourceServerCacheOptions } from './cache/types'
39
+ import { isViewDependentTable } from './fetchers/utils/browser-row-policy'
39
40
 
40
41
  export { generateRawQueryFetcher, parseQueryTemplateVariables }
41
42
 
@@ -108,7 +109,7 @@ export function generateDataSourceFetcher(
108
109
  if (!validation.isValid) {
109
110
  throw new Error(`PostgreSQL/CockroachDB config validation failed: ${validation.error}`)
110
111
  }
111
- return generatePostgreSQLFetcher(config, tableName)
112
+ return generatePostgreSQLFetcher(config, tableName, transformOptions.trustedReaderRoles)
112
113
  }
113
114
 
114
115
  case 'mysql':
@@ -185,7 +186,7 @@ export function generateDataSourceFetcher(
185
186
  `Supabase (PostgreSQL fallback) config validation failed: ${pgValidation.error}`
186
187
  )
187
188
  }
188
- return generatePostgreSQLFetcher(config, tableName)
189
+ return generatePostgreSQLFetcher(config, tableName, transformOptions.trustedReaderRoles)
189
190
  }
190
191
 
191
192
  const validation = validateSupabaseConfig(config)
@@ -301,6 +302,9 @@ export function generateDataSourceFetcherWithCore(
301
302
  const cachePreamble = cacheOptions
302
303
  ? `import { tqWithCache } from '${isApiRoute ? '../../' : '../'}tq-cache/server'`
303
304
  : ''
305
+ // A module carrying the browser row policy (the Postgres-family routes) keys
306
+ // its cache by who is reading; see `__tqCacheByView` below.
307
+ const splitsCacheByView = handlerCode.indexOf('function __brpSharesPublicView(') !== -1
304
308
 
305
309
  const cacheWiring = cacheOptions
306
310
  ? `
@@ -322,8 +326,35 @@ const __tqCountCache = ${JSON.stringify({
322
326
  swr: cacheOptions.staleWhileRevalidate || 0,
323
327
  })}
324
328
 
325
- const cachedHandler = tqWithCache(handler, __tqDataCache)
326
- const cachedGetCount = tqWithCache(getCount, __tqCountCache)
329
+ ${
330
+ splitsCacheByView
331
+ ? `// The store's own server code and its administrators may be served rows a
332
+ // visitor is not (see __brpSharesPublicView), so they read and fill a cache of
333
+ // their own — one never marked for a shared (CDN) cache either.
334
+ const __tqViewDependent = ${JSON.stringify(
335
+ isViewDependentTable(tableName, [
336
+ ...TableAccess.PROTECTED_TABLES,
337
+ ...Object.keys(TableAccess.PUBLIC_FEED_TABLES),
338
+ ])
339
+ )}
340
+ function __tqCacheByView(fn, opts) {
341
+ const publicView = tqWithCache(fn, opts)
342
+ const trustedView = tqWithCache(fn, Object.assign({}, opts, {
343
+ scope: opts.scope + ':trusted',
344
+ versionScope: opts.versionScope || opts.scope,
345
+ sMaxAge: 0,
346
+ swr: 0,
347
+ }))
348
+ return async function (req, res) {
349
+ return (await __brpSharesPublicView(req, __tqViewDependent)) ? publicView(req, res) : trustedView(req, res)
350
+ }
351
+ }
352
+
353
+ const cachedHandler = __tqCacheByView(handler, __tqDataCache)
354
+ const cachedGetCount = __tqCacheByView(getCount, __tqCountCache)`
355
+ : `const cachedHandler = tqWithCache(handler, __tqDataCache)
356
+ const cachedGetCount = tqWithCache(getCount, __tqCountCache)`
357
+ }
327
358
  `
328
359
  : ''
329
360
 
@@ -349,9 +380,13 @@ export default { fetchData, fetchCount, handler: ${handlerExport}, getCount: ${c
349
380
  return `${[...allImports, cachePreamble].filter(Boolean).join('\n')}
350
381
 
351
382
  async function fetchData(params = {}) {
383
+ // A server-side call from this process (getStaticProps, a page-load
384
+ // workflow): the money-table guard trusts the marker, which no HTTP request
385
+ // can carry.
352
386
  const req = {
353
387
  query: params,
354
388
  method: 'GET',
389
+ __tqServerCall: true,
355
390
  }
356
391
 
357
392
  let result = null
@@ -381,6 +416,7 @@ async function fetchCount(params = {}) {
381
416
  const req = {
382
417
  query: params,
383
418
  method: 'GET',
419
+ __tqServerCall: true,
384
420
  }
385
421
 
386
422
  let result = null
@@ -56,11 +56,15 @@ ${generateCollectionPathHelperCode()}
56
56
 
57
57
  export default async function handler(req, res) {
58
58
  try {
59
- const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters, sorts, collectionPath } = req.query
59
+ const { limit, offset, page, perPage, query, queryColumns, sortBy, sortOrder, filters, sorts, collectionPath, itemsPath } = req.query
60
60
 
61
61
  const code = ${JSON.stringify(jsConfig.code)}
62
62
  const executeCode = new Function('return ' + code)
63
63
  let data = resolveCollectionPath(executeCode(), collectionPath)
64
+ const itemsEnvelope = openItemsEnvelope(data, itemsPath)
65
+ if (itemsEnvelope) {
66
+ data = itemsEnvelope.items
67
+ }
64
68
 
65
69
  if (Array.isArray(data)) {
66
70
  if (query && query.trim()) {
@@ -192,6 +196,10 @@ export default async function handler(req, res) {
192
196
  }
193
197
  }
194
198
 
199
+ if (itemsEnvelope) {
200
+ data = itemsEnvelope.close(data)
201
+ }
202
+
195
203
  const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
196
204
 
197
205
  return res.status(200).json({
@@ -11,6 +11,9 @@ import {
11
11
  generateSortTiebreakSql,
12
12
  } from '../product-price-sort'
13
13
  import { generateProductFilterClauseHelper } from '../product-filter-fields'
14
+ import { generateReadGuardCall, generateTableAccessPreamble } from './utils/table-access-guard'
15
+ import { generateRequestSqlGuardsCode } from './utils/request-sql-guards'
16
+ import { generateArrayOverlapSqlCode } from '../array-overlap-sql'
14
17
 
15
18
  interface PostgreSQLConfig {
16
19
  connectionString?: string
@@ -27,7 +30,8 @@ interface PostgreSQLConfig {
27
30
 
28
31
  export const generatePostgreSQLFetcher = (
29
32
  config: Record<string, unknown>,
30
- tableName: string
33
+ tableName: string,
34
+ trustedReaderRoles: ReadonlyArray<string> = []
31
35
  ): string => {
32
36
  const pgConfig = config as PostgreSQLConfig
33
37
  const schema = pgConfig.options?.schema
@@ -58,6 +62,8 @@ export const generatePostgreSQLFetcher = (
58
62
 
59
63
  return `import { Client } from 'pg'
60
64
 
65
+ ${generateTableAccessPreamble(trustedReaderRoles)}
66
+
61
67
  const getClient = () => {
62
68
  return new Client(${clientConfig})
63
69
  }
@@ -67,9 +73,11 @@ ${generateSafeJSONParseCode()}
67
73
  ${generateFilterTreeHelpersCode()}
68
74
 
69
75
  ${generateSearchEscapeHelpersCode()}
76
+ ${generateArrayOverlapSqlCode()}
70
77
 
71
78
  // Builds one SQL clause for the whole filter tree, so an OR group nested in
72
79
  // the root AND keeps its meaning instead of being flattened into ANDs.
80
+ ${generateRequestSqlGuardsCode()}
73
81
  const processFilters = (filters, conditions, queryParams, paramIndex) => {
74
82
  if (!filters) return paramIndex
75
83
 
@@ -91,11 +99,13 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
91
99
  return '$' + paramIndex++
92
100
  })
93
101
  if (productClause !== null) return productClause
102
+ assertRequestColumn(field, 'filter field')
94
103
 
95
104
  if (Array.isArray(value)) {
96
105
  if (value.length === 0) return null
97
106
  if (operand === 'array_overlap') {
98
- const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
107
+ // The row's list (see array-overlap-sql.ts) shares any value with the set.
108
+ const clause = arrayOverlapSql([field], '$' + paramIndex)
99
109
  queryParams.push(value.map((entry) => String(entry)))
100
110
  paramIndex++
101
111
  return clause
@@ -113,7 +123,7 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
113
123
  // ?categoryFilter=a,b,c) expands to multiple ids; one id stays one.
114
124
  const overlapValues = String(value).split(',').map((entry) => entry.trim()).filter(Boolean)
115
125
  if (overlapValues.length === 0) return null
116
- const clause = \`jsonb_exists_any(NULLIF(\${field}, '')::jsonb, $\${paramIndex}::text[])\`
126
+ const clause = arrayOverlapSql([field], '$' + paramIndex)
117
127
  queryParams.push(overlapValues)
118
128
  paramIndex++
119
129
  return clause
@@ -148,6 +158,7 @@ ${generateSortFallbackFieldHelper(tableName)}
148
158
  ${generateProductFilterClauseHelper(tableName)}
149
159
 
150
160
  export default async function handler(req, res) {
161
+ ${generateReadGuardCall(tableName, null)}
151
162
  const client = getClient()
152
163
 
153
164
  try {
@@ -155,6 +166,8 @@ export default async function handler(req, res) {
155
166
  ${schema ? `await client.query('SET search_path TO ${schema}')` : ''}
156
167
 
157
168
  const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset } = req.query
169
+ // A visitor never filters, sorts or searches by a column it is not served.
170
+ if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
158
171
 
159
172
  const conditions = []
160
173
  const queryParams = []
@@ -189,6 +202,7 @@ export default async function handler(req, res) {
189
202
  // Continue without search if we can't get columns
190
203
  }
191
204
  }
205
+ if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
192
206
 
193
207
  if (columns.length > 0) {
194
208
  const pattern = '%' + escapeLikePattern(query) + '%'
@@ -204,6 +218,7 @@ export default async function handler(req, res) {
204
218
 
205
219
  // Apply filters using helper function
206
220
  paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
221
+ if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
207
222
 
208
223
  let sql = \`SELECT * FROM "${tableName}"\`
209
224
 
@@ -219,6 +234,7 @@ export default async function handler(req, res) {
219
234
  const parsedSorts = safeJSONParse(sorts)
220
235
  if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
221
236
  const valid = parsedSorts.filter((sort) => sort && sort.field)
237
+ valid.forEach((sort) => assertRequestColumn(sort.field, 'sort field'))
222
238
  const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
223
239
  const orderClauses = valid.map((sort) => \`\${sortFieldSql(sort.field)} \${orderOf(sort)}\`)
224
240
  const plainClauses = valid.map((sort) => \`\${sortFallbackField(sort.field)} \${orderOf(sort)}\`)
@@ -236,16 +252,17 @@ export default async function handler(req, res) {
236
252
  }
237
253
  }
238
254
  } else if (sortBy) {
255
+ assertRequestColumn(sortBy, 'sort field')
239
256
  orderBySql = \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
240
257
  plainOrderBySql = orderBySql
241
258
  }
242
259
  const usedDiscountAwareSort = orderBySql !== plainOrderBySql
243
260
 
244
- const limitValue = limit || perPage
261
+ const limitValue = requestRowCount(limit || perPage)
245
262
  const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
246
263
 
247
264
  let sqlTail = ''
248
- if (limitValue) {
265
+ if (limitValue !== undefined) {
249
266
  sqlTail += \` LIMIT \${limitValue}\`
250
267
  }
251
268
 
@@ -279,14 +296,20 @@ export default async function handler(req, res) {
279
296
 
280
297
  return res.status(200).json({
281
298
  success: true,
282
- data: safeData,
299
+ data: __taWithoutCredentials(
300
+ __rowPolicy ? __brpStripHidden(__rowPolicy, safeData) : safeData,
301
+ ${JSON.stringify(tableName)}
302
+ ),
283
303
  timestamp: Date.now()
284
304
  })
285
305
  } catch (error) {
286
306
  console.error('PostgreSQL fetch error:', error)
287
- return res.status(500).json({
307
+ // A database error names tables, columns and values: it stays in the server
308
+ // log. Only an error this route raised for the caller (it carries a status)
309
+ // is repeated to it.
310
+ return res.status(error.status || 500).json({
288
311
  success: false,
289
- error: error.message || 'Failed to fetch data',
312
+ error: error.status ? error.message : 'Failed to fetch data',
290
313
  timestamp: Date.now()
291
314
  })
292
315
  } finally {
@@ -311,11 +334,13 @@ export const generatePostgreSQLCountFetcher = (
311
334
 
312
335
  return `
313
336
  async function getCount(req, res) {
337
+ ${generateReadGuardCall(tableName, null)}
314
338
  const client = getClient()
315
339
 
316
340
  try {
317
341
  await client.connect()
318
342
  const { query, queryColumns, filters } = req.query
343
+ if (__rowPolicy) __brpAssertVisibleFields(__rowPolicy, req.query)
319
344
  const conditions = []
320
345
  const queryParams = []
321
346
  let paramIndex = 1
@@ -350,6 +375,7 @@ async function getCount(req, res) {
350
375
  // Continue without search if we can't get columns
351
376
  }
352
377
  }
378
+ if (__rowPolicy) columns = __brpVisibleColumns(__rowPolicy, columns)
353
379
 
354
380
  if (columns.length > 0) {
355
381
  const pattern = '%' + escapeLikePattern(query) + '%'
@@ -367,6 +393,7 @@ async function getCount(req, res) {
367
393
 
368
394
  // Apply filters using helper function
369
395
  paramIndex = processFilters(filters, conditions, queryParams, paramIndex)
396
+ if (__rowPolicy) conditions.push('(' + __rowPolicy.predicate + ')')
370
397
 
371
398
  let countSql = \`SELECT COUNT(*) FROM "${tableName}"\`
372
399
  if (conditions.length > 0) {
@@ -383,9 +410,9 @@ async function getCount(req, res) {
383
410
  })
384
411
  } catch (error) {
385
412
  console.error('Error getting count:', error)
386
- return res.status(500).json({
413
+ return res.status(error.status || 500).json({
387
414
  success: false,
388
- error: error.message || 'Failed to get count',
415
+ error: error.status ? error.message : 'Failed to get count',
389
416
  timestamp: Date.now()
390
417
  })
391
418
  } finally {
@@ -1,3 +1,4 @@
1
+ import { SessionCookieResolver } from '@teleporthq/teleport-shared'
1
2
  import { replaceSecretReference } from '../utils'
2
3
 
3
4
  interface PostgreSQLConfig {
@@ -13,9 +14,28 @@ interface PostgreSQLConfig {
13
14
 
14
15
  const FORBIDDEN_KEYWORDS = ['CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME', 'GRANT', 'REVOKE']
15
16
 
17
+ /** The param a `{{Current User.<field>}}` token becomes: `currentUser` + the field, capitalised. */
18
+ const CURRENT_USER_PARAM_PREFIX = 'currentUser'
19
+
20
+ const isCurrentUserParam = (param: string): boolean =>
21
+ param.length > CURRENT_USER_PARAM_PREFIX.length && param.indexOf(CURRENT_USER_PARAM_PREFIX) === 0
22
+
23
+ /** The user field a `currentUser<Field>` param stands for (`currentUserId` → `id`). */
24
+ const currentUserField = (param: string): string => {
25
+ const rest = param.slice(CURRENT_USER_PARAM_PREFIX.length)
26
+ return rest.charAt(0).toLowerCase() + rest.slice(1)
27
+ }
28
+
16
29
  /**
17
30
  * Generates an API route handler that executes a parameterized raw SQL query.
18
- * Supports {{Current User.*}} substitution via query params.
31
+ * Supports {{Current User.*}} substitution — bound to the SESSION's user.
32
+ *
33
+ * ⛔ A `{{Current User.<field>}}` value is read from the signed-in session on
34
+ * the server, never from the request: the query string is whatever the caller
35
+ * wrote, so a route that bound `?currentUserId=` served any user's rows (their
36
+ * favourites, their orders) to anyone who named that user's id. A caller with
37
+ * no session gets no rows, exactly as when the value is missing. Other params
38
+ * (`currentPageEntityId`, the details page's route param) stay request values.
19
39
  *
20
40
  * @param config - PostgreSQL connection config
21
41
  * @param query - Raw SQL query string with {{Current User.*}} patterns already replaced to $N placeholders
@@ -27,9 +47,43 @@ export const generateRawQueryFetcher = (
27
47
  paramFields: string[]
28
48
  ): string => {
29
49
  const pgConfig = config as PostgreSQLConfig
50
+ const userParams = paramFields.filter(isCurrentUserParam)
51
+ const requestParams = paramFields.filter((param) => !isCurrentUserParam(param))
52
+
53
+ const paramDestructure = [
54
+ requestParams.length > 0 ? `const { ${requestParams.join(', ')} } = req.query` : '',
55
+ userParams.length > 0 ? 'const __sessionUser = await sessionUser(req)' : '',
56
+ ...userParams.map(
57
+ (param) =>
58
+ `const ${param} = sessionUserField(__sessionUser, ${JSON.stringify(
59
+ currentUserField(param)
60
+ )})`
61
+ ),
62
+ ]
63
+ .filter(Boolean)
64
+ .join('\n ')
65
+
66
+ const sessionHelpers =
67
+ userParams.length > 0
68
+ ? `${SessionCookieResolver.generateCommonJsSessionTokenResolverCode()}
69
+ // The signed-in user this request's session names, or null.
70
+ async function sessionUser(req) {
71
+ try {
72
+ return await __tqSessionToken(req)
73
+ } catch (e) {
74
+ return null
75
+ }
76
+ }
30
77
 
31
- const paramDestructure =
32
- paramFields.length > 0 ? `const { ${paramFields.join(', ')} } = req.query` : ''
78
+ // One field of that user, as the session carries it (the token is seeded from
79
+ // the users row), or undefined. The id is the token's subject when not copied.
80
+ function sessionUserField(user, field) {
81
+ if (!user) return undefined
82
+ var value = field === 'id' && user.id == null ? user.sub : user[field]
83
+ return value == null || value === '' ? undefined : value
84
+ }
85
+ `
86
+ : ''
33
87
 
34
88
  const paramValidation =
35
89
  paramFields.length > 0
@@ -58,7 +112,7 @@ export const generateRawQueryFetcher = (
58
112
  return `import { Client } from 'pg'
59
113
 
60
114
  const FORBIDDEN_KEYWORDS = ${JSON.stringify(FORBIDDEN_KEYWORDS)}
61
-
115
+ ${sessionHelpers}
62
116
  const getClient = () => {
63
117
  const connStr = process.env.TELEPORT_DB_CONNECTION_STRING
64
118
  if (connStr) {
@@ -122,10 +176,11 @@ export default async function handler(req, res) {
122
176
  await client.end()
123
177
  }
124
178
  } catch (error) {
179
+ // A database error names tables, columns and values: server log only.
125
180
  console.error('Raw query fetch error:', error)
126
181
  return res.status(500).json({
127
182
  success: false,
128
- error: error.message || 'Failed to fetch data',
183
+ error: 'Failed to fetch data',
129
184
  timestamp: Date.now()
130
185
  })
131
186
  }
@@ -5,6 +5,11 @@ import {
5
5
  generateSafeJSONParseCode,
6
6
  generateFilterTreeHelpersCode,
7
7
  } from '../utils'
8
+ import {
9
+ RESTAPICacheConfig,
10
+ generateRESTAPIResponseCacheCode,
11
+ resolveRESTAPICacheTTLSeconds,
12
+ } from './utils/rest-api-response-cache'
8
13
 
9
14
  export const validateRESTAPIConfig = (
10
15
  config: Record<string, unknown>
@@ -76,11 +81,16 @@ interface RESTAPIConfig {
76
81
  authorization?: Authorization
77
82
  bodyType?: string
78
83
  body?: string
84
+ cache?: RESTAPICacheConfig
79
85
  }
80
86
 
81
87
  export const generateRESTAPIFetcher = (config: Record<string, unknown>): string => {
82
88
  const restConfig = config as RESTAPIConfig
83
89
  const authCode = generateAuthCode(restConfig.authorization || {})
90
+ // Every cache-only fragment below is empty when caching is off, so an
91
+ // uncached data source keeps generating byte-identical output.
92
+ const cacheTTLSeconds = resolveRESTAPICacheTTLSeconds(restConfig.cache)
93
+ const cached = cacheTTLSeconds !== undefined
84
94
 
85
95
  return `import fetch from 'node-fetch'
86
96
 
@@ -92,11 +102,13 @@ ${generateSortFilterHelperCode()}
92
102
 
93
103
  ${generateFilterTreeHelpersCode()}
94
104
 
95
- ${generateCollectionPathHelperCode()}
105
+ ${generateCollectionPathHelperCode()}${
106
+ cached ? `\n\n${generateRESTAPIResponseCacheCode(cacheTTLSeconds)}` : ''
107
+ }
96
108
 
97
109
  export default async function handler(req, res) {
98
110
  try {
99
- const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset, collectionPath } = req.query
111
+ const { query, queryColumns, limit, page, perPage, sortBy, sortOrder, filters, sorts, offset, collectionPath, itemsPath } = req.query
100
112
 
101
113
  const url = ${JSON.stringify(restConfig.url)}
102
114
  const method = ${JSON.stringify(restConfig.method || 'GET')}
@@ -134,7 +146,7 @@ export default async function handler(req, res) {
134
146
  : ''
135
147
  }
136
148
 
137
- const response = await fetch(url, options)
149
+ const response = await ${cached ? 'fetchRestApiWithCache' : 'fetch'}(url, options)
138
150
 
139
151
  if (!response.ok) {
140
152
  return res.status(response.status).json({
@@ -144,9 +156,13 @@ export default async function handler(req, res) {
144
156
  })
145
157
  }
146
158
 
147
- let data = await response.json()
159
+ let data = ${cached ? 'JSON.parse(response.bodyText)' : 'await response.json()'}
148
160
 
149
161
  data = resolveCollectionPath(data, collectionPath)
162
+ const itemsEnvelope = openItemsEnvelope(data, itemsPath)
163
+ if (itemsEnvelope) {
164
+ data = itemsEnvelope.items
165
+ }
150
166
 
151
167
  if (Array.isArray(data)) {
152
168
  if (query && query.trim()) {
@@ -278,9 +294,15 @@ export default async function handler(req, res) {
278
294
  }
279
295
  }
280
296
 
297
+ if (itemsEnvelope) {
298
+ data = itemsEnvelope.close(data)
299
+ }
300
+
281
301
  const safeData = JSON.parse(JSON.stringify(data, dateReplacer))
282
302
 
283
- return res.status(200).json({
303
+ ${
304
+ cached ? 'applyRestApiCacheHeaders(req, res, response.fetchedAt)\n \n ' : ''
305
+ }return res.status(200).json({
284
306
  success: true,
285
307
  data: safeData,
286
308
  timestamp: Date.now()