@teleporthq/teleport-plugin-next-data-source 0.43.75 → 0.44.1

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 (199) hide show
  1. package/__tests__/_helpers/first-page-runtime.ts +184 -0
  2. package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
  3. package/__tests__/booking-item-transform.test.ts +120 -0
  4. package/__tests__/browser-row-policy.test.ts +210 -4
  5. package/__tests__/data-source-request-locale.test.ts +4 -3
  6. package/__tests__/fetchers.test.ts +7 -3
  7. package/__tests__/fixed-list-sort.test.ts +1 -1
  8. package/__tests__/infinite-scroll.test.ts +363 -9
  9. package/__tests__/list-sort-order.test.ts +61 -0
  10. package/__tests__/null-filter-where.test.ts +132 -0
  11. package/__tests__/page-data-visitor-read.test.ts +196 -0
  12. package/__tests__/pg-date-parser.test.ts +73 -0
  13. package/__tests__/plain-list-sort.test.ts +2 -2
  14. package/__tests__/plain-list-starting-sort.test.ts +323 -0
  15. package/__tests__/product-price-sort.ts +11 -6
  16. package/__tests__/range-control.test.ts +159 -0
  17. package/__tests__/refetch-loading-state.test.ts +158 -2
  18. package/__tests__/search-input-marker.test.ts +86 -0
  19. package/__tests__/search-url-sync.test.ts +34 -8
  20. package/__tests__/starting-sort-first-page.test.ts +285 -0
  21. package/__tests__/table-read-route-protected-tables.test.ts +33 -3
  22. package/dist/cjs/cache/ast.d.ts +2 -0
  23. package/dist/cjs/cache/ast.d.ts.map +1 -1
  24. package/dist/cjs/cache/ast.js +5 -2
  25. package/dist/cjs/cache/ast.js.map +1 -1
  26. package/dist/cjs/count-effect.d.ts +7 -1
  27. package/dist/cjs/count-effect.d.ts.map +1 -1
  28. package/dist/cjs/count-effect.js +15 -5
  29. package/dist/cjs/count-effect.js.map +1 -1
  30. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  31. package/dist/cjs/data-source-fetchers.js +10 -2
  32. package/dist/cjs/data-source-fetchers.js.map +1 -1
  33. package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
  34. package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
  35. package/dist/cjs/fetchers/pg-client-import.js +15 -0
  36. package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
  37. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  38. package/dist/cjs/fetchers/postgresql.js +3 -1
  39. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  40. package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
  41. package/dist/cjs/fetchers/raw-query.js +2 -1
  42. package/dist/cjs/fetchers/raw-query.js.map +1 -1
  43. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  44. package/dist/cjs/fetchers/teleport.js +4 -2
  45. package/dist/cjs/fetchers/teleport.js.map +1 -1
  46. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  47. package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
  48. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  49. package/dist/cjs/index.d.ts.map +1 -1
  50. package/dist/cjs/index.js +14 -2
  51. package/dist/cjs/index.js.map +1 -1
  52. package/dist/cjs/infinite-scroll.d.ts +89 -4
  53. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  54. package/dist/cjs/infinite-scroll.js +243 -33
  55. package/dist/cjs/infinite-scroll.js.map +1 -1
  56. package/dist/cjs/list-sort-order.d.ts +14 -0
  57. package/dist/cjs/list-sort-order.d.ts.map +1 -0
  58. package/dist/cjs/list-sort-order.js +36 -0
  59. package/dist/cjs/list-sort-order.js.map +1 -0
  60. package/dist/cjs/loading-state.d.ts +23 -2
  61. package/dist/cjs/loading-state.d.ts.map +1 -1
  62. package/dist/cjs/loading-state.js +50 -15
  63. package/dist/cjs/loading-state.js.map +1 -1
  64. package/dist/cjs/pagination-controls.d.ts +22 -3
  65. package/dist/cjs/pagination-controls.d.ts.map +1 -1
  66. package/dist/cjs/pagination-controls.js +29 -3
  67. package/dist/cjs/pagination-controls.js.map +1 -1
  68. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  69. package/dist/cjs/pagination-plugin.js +336 -164
  70. package/dist/cjs/pagination-plugin.js.map +1 -1
  71. package/dist/cjs/range-control.d.ts +45 -0
  72. package/dist/cjs/range-control.d.ts.map +1 -0
  73. package/dist/cjs/range-control.js +165 -0
  74. package/dist/cjs/range-control.js.map +1 -0
  75. package/dist/cjs/row-order.d.ts +3 -2
  76. package/dist/cjs/row-order.d.ts.map +1 -1
  77. package/dist/cjs/row-order.js +5 -4
  78. package/dist/cjs/row-order.js.map +1 -1
  79. package/dist/cjs/search-input.d.ts +42 -0
  80. package/dist/cjs/search-input.d.ts.map +1 -0
  81. package/dist/cjs/search-input.js +158 -0
  82. package/dist/cjs/search-input.js.map +1 -0
  83. package/dist/cjs/sort-utils.d.ts +53 -1
  84. package/dist/cjs/sort-utils.d.ts.map +1 -1
  85. package/dist/cjs/sort-utils.js +105 -13
  86. package/dist/cjs/sort-utils.js.map +1 -1
  87. package/dist/cjs/transformations/booking-item.d.ts +24 -0
  88. package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
  89. package/dist/cjs/transformations/booking-item.js +27 -0
  90. package/dist/cjs/transformations/booking-item.js.map +1 -0
  91. package/dist/cjs/transformations/index.d.ts +4 -2
  92. package/dist/cjs/transformations/index.d.ts.map +1 -1
  93. package/dist/cjs/transformations/index.js +14 -5
  94. package/dist/cjs/transformations/index.js.map +1 -1
  95. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  96. package/dist/cjs/utils.d.ts +3 -2
  97. package/dist/cjs/utils.d.ts.map +1 -1
  98. package/dist/cjs/utils.js +48 -18
  99. package/dist/cjs/utils.js.map +1 -1
  100. package/dist/esm/cache/ast.d.ts +2 -0
  101. package/dist/esm/cache/ast.d.ts.map +1 -1
  102. package/dist/esm/cache/ast.js +3 -1
  103. package/dist/esm/cache/ast.js.map +1 -1
  104. package/dist/esm/count-effect.d.ts +7 -1
  105. package/dist/esm/count-effect.d.ts.map +1 -1
  106. package/dist/esm/count-effect.js +15 -5
  107. package/dist/esm/count-effect.js.map +1 -1
  108. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  109. package/dist/esm/data-source-fetchers.js +10 -2
  110. package/dist/esm/data-source-fetchers.js.map +1 -1
  111. package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
  112. package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
  113. package/dist/esm/fetchers/pg-client-import.js +12 -0
  114. package/dist/esm/fetchers/pg-client-import.js.map +1 -0
  115. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  116. package/dist/esm/fetchers/postgresql.js +3 -1
  117. package/dist/esm/fetchers/postgresql.js.map +1 -1
  118. package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
  119. package/dist/esm/fetchers/raw-query.js +2 -1
  120. package/dist/esm/fetchers/raw-query.js.map +1 -1
  121. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  122. package/dist/esm/fetchers/teleport.js +4 -2
  123. package/dist/esm/fetchers/teleport.js.map +1 -1
  124. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  125. package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
  126. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  127. package/dist/esm/index.d.ts.map +1 -1
  128. package/dist/esm/index.js +14 -2
  129. package/dist/esm/index.js.map +1 -1
  130. package/dist/esm/infinite-scroll.d.ts +89 -4
  131. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  132. package/dist/esm/infinite-scroll.js +237 -32
  133. package/dist/esm/infinite-scroll.js.map +1 -1
  134. package/dist/esm/list-sort-order.d.ts +14 -0
  135. package/dist/esm/list-sort-order.d.ts.map +1 -0
  136. package/dist/esm/list-sort-order.js +32 -0
  137. package/dist/esm/list-sort-order.js.map +1 -0
  138. package/dist/esm/loading-state.d.ts +23 -2
  139. package/dist/esm/loading-state.d.ts.map +1 -1
  140. package/dist/esm/loading-state.js +50 -15
  141. package/dist/esm/loading-state.js.map +1 -1
  142. package/dist/esm/pagination-controls.d.ts +22 -3
  143. package/dist/esm/pagination-controls.d.ts.map +1 -1
  144. package/dist/esm/pagination-controls.js +28 -2
  145. package/dist/esm/pagination-controls.js.map +1 -1
  146. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  147. package/dist/esm/pagination-plugin.js +340 -168
  148. package/dist/esm/pagination-plugin.js.map +1 -1
  149. package/dist/esm/range-control.d.ts +45 -0
  150. package/dist/esm/range-control.d.ts.map +1 -0
  151. package/dist/esm/range-control.js +137 -0
  152. package/dist/esm/range-control.js.map +1 -0
  153. package/dist/esm/row-order.d.ts +3 -2
  154. package/dist/esm/row-order.d.ts.map +1 -1
  155. package/dist/esm/row-order.js +5 -4
  156. package/dist/esm/row-order.js.map +1 -1
  157. package/dist/esm/search-input.d.ts +42 -0
  158. package/dist/esm/search-input.d.ts.map +1 -0
  159. package/dist/esm/search-input.js +129 -0
  160. package/dist/esm/search-input.js.map +1 -0
  161. package/dist/esm/sort-utils.d.ts +53 -1
  162. package/dist/esm/sort-utils.d.ts.map +1 -1
  163. package/dist/esm/sort-utils.js +100 -12
  164. package/dist/esm/sort-utils.js.map +1 -1
  165. package/dist/esm/transformations/booking-item.d.ts +24 -0
  166. package/dist/esm/transformations/booking-item.d.ts.map +1 -0
  167. package/dist/esm/transformations/booking-item.js +23 -0
  168. package/dist/esm/transformations/booking-item.js.map +1 -0
  169. package/dist/esm/transformations/index.d.ts +4 -2
  170. package/dist/esm/transformations/index.d.ts.map +1 -1
  171. package/dist/esm/transformations/index.js +14 -5
  172. package/dist/esm/transformations/index.js.map +1 -1
  173. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  174. package/dist/esm/utils.d.ts +3 -2
  175. package/dist/esm/utils.d.ts.map +1 -1
  176. package/dist/esm/utils.js +49 -19
  177. package/dist/esm/utils.js.map +1 -1
  178. package/package.json +5 -5
  179. package/src/cache/ast.ts +4 -1
  180. package/src/count-effect.ts +31 -8
  181. package/src/data-source-fetchers.ts +33 -14
  182. package/src/fetchers/pg-client-import.ts +16 -0
  183. package/src/fetchers/postgresql.ts +15 -9
  184. package/src/fetchers/raw-query.ts +2 -1
  185. package/src/fetchers/teleport.ts +19 -10
  186. package/src/fetchers/utils/browser-row-policy.ts +79 -5
  187. package/src/index.ts +8 -2
  188. package/src/infinite-scroll.ts +420 -81
  189. package/src/list-sort-order.ts +34 -0
  190. package/src/loading-state.ts +85 -13
  191. package/src/pagination-controls.ts +26 -3
  192. package/src/pagination-plugin.ts +532 -218
  193. package/src/range-control.ts +207 -0
  194. package/src/row-order.ts +5 -4
  195. package/src/search-input.ts +190 -0
  196. package/src/sort-utils.ts +164 -21
  197. package/src/transformations/booking-item.ts +71 -0
  198. package/src/transformations/index.ts +25 -2
  199. package/src/utils.ts +55 -18
@@ -67,7 +67,10 @@ function bootTableRoute(tableName: string, options: BootOptions = {}) {
67
67
  options.cache
68
68
  )
69
69
  const code = esm
70
- .replace("import { Client } from 'pg'", "const { Client } = require('pg')")
70
+ .replace(
71
+ "import { Client, types as pgTypes } from 'pg'",
72
+ "const { Client, types: pgTypes } = require('pg')"
73
+ )
71
74
  .replace(
72
75
  "import { tqWithCache } from '../tq-cache/server'",
73
76
  "const { tqWithCache } = require('tq-cache')"
@@ -190,11 +193,25 @@ function bootTableRoute(tableName: string, options: BootOptions = {}) {
190
193
  return moduleObj.exports.fetchData({})
191
194
  }
192
195
 
193
- return { run, serverFetch, queries, cacheCalls }
196
+ // A page's own data, as its getStaticProps / getServerSideProps reads it.
197
+ const pageFetch = async (params: Record<string, unknown> = {}): Promise<unknown> => {
198
+ queries.length = 0
199
+ session = null
200
+ return moduleObj.exports.fetchData(params, { as: 'visitor' })
201
+ }
202
+ const pageCount = async (params: Record<string, unknown> = {}): Promise<unknown> => {
203
+ queries.length = 0
204
+ session = null
205
+ return moduleObj.exports.fetchCount(params, { as: 'visitor' })
206
+ }
207
+
208
+ return { run, serverFetch, pageFetch, pageCount, queries, cacheCalls }
194
209
  }
195
210
 
196
211
  const internal = { 'x-internal-data-secret': SECRET }
197
212
 
213
+ const SORT_BY_NAME = JSON.stringify([{ field: 'name', order: 'asc' }])
214
+
198
215
  const mainQuery = (result: RouteResult): string =>
199
216
  (result.queries.find((q) => /^\s*SELECT \* FROM/i.test(q.sql)) || { sql: '' }).sql
200
217
 
@@ -230,7 +247,7 @@ describe('blog posts: a visitor reads the published posts, never an author email
230
247
  expect(JSON.stringify(shopper.body.data)).not.toContain('ana@example.com')
231
248
  })
232
249
 
233
- it('serves the store everything: the admin, a server segment and getStaticProps', async () => {
250
+ it("serves the store everything: the admin, a server segment and the store's own server code", async () => {
234
251
  const admin = await posts.run({ session: { role: 'admin' } })
235
252
  expect(mainQuery(admin)).not.toContain("status = 'published'")
236
253
  // The post view model carries no email for anyone — a page's data must not
@@ -249,6 +266,26 @@ describe('blog posts: a visitor reads the published posts, never an author email
249
266
  expect(serverRead && serverRead.sql).not.toContain("(status = 'published')")
250
267
  })
251
268
 
269
+ // This used to read as the store too: a page's getStaticProps called
270
+ // fetchData with the store's own marker. But a page's data is what it renders,
271
+ // and it reaches whoever opens the page (and `/_next/data` without rendering
272
+ // it): a draft painted on page 1 that the visitor's own page 2 skipped, a
273
+ // draft's details page anyone could open. A page reads `{ as: 'visitor' }`.
274
+ it("reads a page's data as a visitor: published posts, no author email", async () => {
275
+ await posts.pageFetch()
276
+ const pageRead = posts.queries.find((q) => /^\s*SELECT \* FROM/i.test(q.sql))
277
+ expect(pageRead && pageRead.sql).toContain("(status = 'published')")
278
+
279
+ const rawRows = (await posts.pageFetch({ rawRows: 'true' })) as any[]
280
+ expect(JSON.stringify(rawRows)).not.toContain('ana@example.com')
281
+ expect(rawRows[0].title).toBe('Hello')
282
+
283
+ await posts.pageCount()
284
+ expect(posts.queries.some((q) => /COUNT\(\*\).*\(status = 'published'\)/.test(q.sql))).toBe(
285
+ true
286
+ )
287
+ })
288
+
252
289
  it('narrows the count the same way', async () => {
253
290
  const guest = await posts.run({ handler: 'getCount' })
254
291
  expect(guest.status).toBe(200)
@@ -331,6 +368,172 @@ describe('products: a visitor reads the active products', () => {
331
368
  )
332
369
  expect(mainQuery(await products.run({ headers: internal }))).not.toContain("= 'active'")
333
370
  })
371
+
372
+ it("narrows a page's own data like a visitor's request, page and count alike", async () => {
373
+ const visitor = mainQuery(
374
+ await products.run({ query: { page: '1', perPage: '20', sorts: SORT_BY_NAME } })
375
+ )
376
+ await products.pageFetch({ page: 1, perPage: 20, sorts: SORT_BY_NAME })
377
+ const page = (products.queries.find((q) => /^\s*SELECT \* FROM/i.test(q.sql)) || { sql: '' })
378
+ .sql
379
+ expect(page).toContain("(LOWER(TRIM(status)) = 'active')")
380
+ expect(page).toBe(visitor)
381
+
382
+ await products.pageCount()
383
+ expect(
384
+ products.queries.some((q) =>
385
+ /COUNT\(\*\).*\(LOWER\(TRIM\(status\)\) = 'active'\)/.test(q.sql)
386
+ )
387
+ ).toBe(true)
388
+ })
389
+
390
+ it("runs a page's own statement over the products a visitor is served", async () => {
391
+ const featured =
392
+ 'SELECT tp.slug, fpc.blurb FROM featured_product_curations fpc INNER JOIN teleport_products tp ON tp.id = fpc.product_id ORDER BY fpc.display_rank ASC LIMIT 8'
393
+ await products.pageFetch({ rawQuery: featured })
394
+ expect(products.queries.map((q) => q.sql)).toEqual([
395
+ `WITH "teleport_products" AS (SELECT * FROM "teleport_products" AS __brp_rows WHERE (LOWER(TRIM(status)) = 'active')) ${featured}`,
396
+ ])
397
+
398
+ // Its own WITH list comes after, so its entries read the visitor's rows too.
399
+ const picked =
400
+ 'WITH picks AS (SELECT * FROM teleport_products WHERE price > 6) SELECT slug FROM picks;'
401
+ await products.pageFetch({ rawQuery: picked })
402
+ expect(products.queries.map((q) => q.sql)).toEqual([
403
+ `WITH "teleport_products" AS (SELECT * FROM "teleport_products" AS __brp_rows WHERE (LOWER(TRIM(status)) = 'active')), picks AS (SELECT * FROM teleport_products WHERE price > 6) SELECT slug FROM picks`,
404
+ ])
405
+
406
+ // A statement over no narrowed table runs as written.
407
+ await products.pageFetch({ rawQuery: 'SELECT id FROM category_tiles' })
408
+ expect(products.queries.map((q) => q.sql)).toEqual(['SELECT id FROM category_tiles'])
409
+
410
+ // The store's own server code still reads every product.
411
+ await products.serverFetch()
412
+ expect(products.queries.map((q) => q.sql).join(' ')).not.toContain("= 'active'")
413
+ })
414
+
415
+ it('refuses a statement it cannot narrow, rather than run it whole', async () => {
416
+ // A schema reaches the table past the visitor's rows; a RECURSIVE list
417
+ // would see its own entries.
418
+ for (const rawQuery of [
419
+ 'SELECT * FROM public.teleport_products',
420
+ 'SELECT * FROM "public"."teleport_products"',
421
+ 'WITH RECURSIVE t AS (SELECT 1) SELECT * FROM teleport_products',
422
+ ]) {
423
+ await expect(products.pageFetch({ rawQuery })).rejects.toThrow(/cannot be read for a visitor/)
424
+ expect(products.queries).toEqual([])
425
+ }
426
+ // A statement that can read a table without naming it is refused sooner:
427
+ // it may be reading any protected one.
428
+ await expect(
429
+ products.pageFetch({
430
+ rawQuery: "SELECT query_to_xml('select * from teleport_products', true, true, '')",
431
+ })
432
+ ).rejects.toThrow('Unauthenticated')
433
+ expect(products.queries).toEqual([])
434
+ })
435
+ })
436
+
437
+ describe("members: a page carries a member's public profile, never their account", () => {
438
+ const MEMBER = {
439
+ id: 'u2',
440
+ name: 'Bo',
441
+ email: 'bo@example.com',
442
+ phone: '+40 700 000 000',
443
+ details: { address: '1 Main St' },
444
+ role: 'user',
445
+ password: 'hash',
446
+ access_token: 'token',
447
+ image: 'https://cdn.example.com/bo.png',
448
+ created_at: '2026-01-01T00:00:00.000Z',
449
+ }
450
+ const PRIVATE = [
451
+ 'bo@example.com',
452
+ '+40 700',
453
+ '1 Main St',
454
+ 'hash',
455
+ 'token',
456
+ '"role"',
457
+ '2026-01-01',
458
+ ]
459
+ const byId = JSON.stringify([
460
+ { type: 'condition', source: 'id', destination: 'u2', operand: '=' },
461
+ ])
462
+
463
+ it('reads the profile a page shows: id, name and avatar only', async () => {
464
+ const users = bootTableRoute('users', { rows: [MEMBER] })
465
+ const rows = (await users.pageFetch({ filters: byId, limit: 1 })) as any[]
466
+
467
+ expect(rows).toEqual([{ id: 'u2', name: 'Bo', image: 'https://cdn.example.com/bo.png' }])
468
+ PRIVATE.forEach((value) => expect(JSON.stringify(rows)).not.toContain(value))
469
+ expect(mainQuery({ queries: users.queries } as RouteResult)).toBe(
470
+ 'SELECT * FROM users WHERE id = $1 AND (TRUE) LIMIT 1'
471
+ )
472
+ })
473
+
474
+ it('never answers a filter, sort or search over a private column', async () => {
475
+ const users = bootTableRoute('users', { rows: [MEMBER] })
476
+ const email = JSON.stringify([
477
+ { type: 'condition', source: 'email', destination: 'bo@example.com', operand: '=' },
478
+ ])
479
+ await expect(users.pageFetch({ filters: email })).rejects.toThrow('email is not readable')
480
+ await expect(
481
+ users.pageFetch({ sorts: JSON.stringify([{ field: 'role', order: 'asc' }]) })
482
+ ).rejects.toThrow('role is not readable')
483
+ expect(users.queries).toEqual([])
484
+
485
+ await users.pageFetch({ query: 'bo', queryColumns: JSON.stringify(['name', 'email']) })
486
+ const searched = mainQuery({ queries: users.queries } as RouteResult)
487
+ expect(searched).toContain('"name"::text ILIKE')
488
+ expect(searched).not.toContain('"email"')
489
+ })
490
+
491
+ it("keeps a browser out, the admin's and the server's reads whole, and the profile out of every cache", async () => {
492
+ const users = bootTableRoute('users', {
493
+ rows: [MEMBER],
494
+ cache: { scope: 'ds-1:users', ttlSeconds: 60, sMaxAge: 30 },
495
+ })
496
+ expect((await users.run({})).status).toBe(401)
497
+ expect((await users.run({ session: { role: 'user' } })).status).toBe(403)
498
+ const admin = await users.run({ session: { role: 'admin' } })
499
+ expect(admin.body.data[0].email).toBe('bo@example.com')
500
+ expect(((await users.serverFetch()) as any[])[0].email).toBe('bo@example.com')
501
+ // Never a credential, whoever reads.
502
+ expect(JSON.stringify(admin.body.data)).not.toContain('hash')
503
+
504
+ users.cacheCalls.length = 0
505
+ await users.pageFetch({ filters: byId, limit: 1 })
506
+ expect(users.cacheCalls).toEqual([])
507
+ })
508
+ })
509
+
510
+ describe("a page's own data never carries a table a visitor may not read", () => {
511
+ it('refuses the protected tables, by name or in a statement', async () => {
512
+ const orders = bootTableRoute('teleport_orders', { rows: [{ id: 'o1', total: 10 }] })
513
+ await expect(orders.pageFetch()).rejects.toThrow('Unauthenticated')
514
+ await expect(orders.pageCount()).rejects.toThrow('Unauthenticated')
515
+ expect(orders.queries).toEqual([])
516
+ // The store's own server code reads them, as it always has.
517
+ expect(await orders.serverFetch()).toEqual([{ id: 'o1', total: 10, __rowNumber: 1 }])
518
+
519
+ const products = bootTableRoute('teleport_products')
520
+ await expect(
521
+ products.pageFetch({
522
+ rawQuery: 'SELECT * FROM teleport_products p JOIN users u ON u.id = p.owner_id',
523
+ })
524
+ ).rejects.toThrow('Unauthenticated')
525
+ expect(products.queries).toEqual([])
526
+ })
527
+
528
+ it("shares the visitors' cache entry, never the store's", async () => {
529
+ const posts = bootTableRoute('teleport_blog_posts', {
530
+ rows: POSTS,
531
+ cache: { scope: 'ds-1:teleport_blog_posts', ttlSeconds: 60, sMaxAge: 30 },
532
+ })
533
+ posts.cacheCalls.length = 0
534
+ await posts.pageFetch()
535
+ expect(posts.cacheCalls).toEqual([{ scope: 'ds-1:teleport_blog_posts', sMaxAge: 30 }])
536
+ })
334
537
  })
335
538
 
336
539
  describe('the AI assistant tables are read by the store only', () => {
@@ -417,7 +620,10 @@ describe('raw-query state routes bind {{Current User.*}} to the session', () =>
417
620
  paramFields
418
621
  )
419
622
  const code = esm
420
- .replace("import { Client } from 'pg'", "const { Client } = require('pg')")
623
+ .replace(
624
+ "import { Client, types as pgTypes } from 'pg'",
625
+ "const { Client, types: pgTypes } = require('pg')"
626
+ )
421
627
  .replace(/^export default async function handler/m, 'async function handler')
422
628
  .concat('\nmodule.exports = { handler: handler }')
423
629
  const queries: Array<{ sql: string; params: unknown[] }> = []
@@ -209,11 +209,12 @@ describe('data-source fetches in a multi-language project', () => {
209
209
  describe('a paginated list page', () => {
210
210
  it('fetches the first page for the language getStaticProps runs for', async () => {
211
211
  const { staticProps } = await run(pagesPlugin, makeListNode(true), makeListProvider(), true)
212
+ // Read as a visitor is served the rows: a public page.
212
213
  expect(collapse(staticProps)).toContain(
213
- 'fetchData({ page: 1, perPage: 20, queryColumns: JSON.stringify(["name", "es_name"]), locale: context.locale })'
214
+ 'fetchData({ page: 1, perPage: 20, queryColumns: JSON.stringify(["name", "es_name"]), locale: context.locale }, { as: "visitor" })'
214
215
  )
215
216
  // The count is the same in every language.
216
- expect(collapse(staticProps)).toContain('fetchCount().catch')
217
+ expect(collapse(staticProps)).toContain('fetchCount({}, { as: "visitor" }).catch')
217
218
  })
218
219
 
219
220
  it('sends the client locale with every refetch and refetches when it changes', async () => {
@@ -247,7 +248,7 @@ describe('data-source fetches in a multi-language project', () => {
247
248
  true
248
249
  )
249
250
  expect(collapse(staticProps)).toContain(
250
- 'fetchData({ "queryColumns": JSON.stringify(["name", "es_name"]), locale: context.locale })'
251
+ 'fetchData({ "queryColumns": JSON.stringify(["name", "es_name"]), locale: context.locale }, { as: "visitor" })'
251
252
  )
252
253
  expect(collapse(code)).toContain(
253
254
  'params={useMemo(() => ({ locale: router?.locale }), [router?.locale])}'
@@ -335,7 +335,7 @@ describe('generateDataSourceFetcher', () => {
335
335
  const dataSource = createPostgreSQLDataSource('ds-1')
336
336
  const code = generateDataSourceFetcher(dataSource, 'users')
337
337
 
338
- expect(code).toContain('import { Client } from')
338
+ expect(code).toContain('import { Client, types as pgTypes } from')
339
339
  expect(code).toContain('export default async function handler')
340
340
  expect(code).toContain('SELECT * FROM "users"')
341
341
  expect(code).toContain('localhost')
@@ -481,8 +481,12 @@ describe('generateDataSourceFetcherWithCore', () => {
481
481
  const dataSource = createPostgreSQLDataSource('ds-1')
482
482
  const code = generateDataSourceFetcherWithCore(dataSource, 'users')
483
483
 
484
- expect(code).toContain('const req = {')
485
- expect(code).toContain('query: params')
484
+ // The store's own call by default; a page's data `{ as: 'visitor' }`.
485
+ expect(code).toContain('const req = __tqServerRequest(params, options)')
486
+ expect(code).toContain("return { query: params, method: 'GET', __tqServerCall: true }")
487
+ expect(code).toContain(
488
+ "return { query: params, method: 'GET', headers: {}, __tqVisitorView: true }"
489
+ )
486
490
  expect(code).toContain('const res = {')
487
491
  expect(code).toContain('status: (code)')
488
492
  expect(code).toContain('json: (data)')
@@ -157,7 +157,7 @@ describe('a list sort the editor fixed', () => {
157
157
  const code = await runPlugin('state', CACHE)
158
158
  expect(code).not.toContain('initialData=')
159
159
  expect(code).not.toContain('ds_0_cached')
160
- expect(code).toContain('field: sortBy.split("-")[0]')
160
+ expect(code).toContain('field: sortBy.split("-")?.[0]')
161
161
  })
162
162
  })
163
163