@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.
- package/__tests__/_helpers/first-page-runtime.ts +184 -0
- package/__tests__/_helpers/pagination-widget-fixture.ts +154 -5
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +210 -4
- package/__tests__/data-source-request-locale.test.ts +4 -3
- package/__tests__/fetchers.test.ts +7 -3
- package/__tests__/fixed-list-sort.test.ts +1 -1
- package/__tests__/infinite-scroll.test.ts +363 -9
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/page-data-visitor-read.test.ts +196 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +2 -2
- package/__tests__/plain-list-starting-sort.test.ts +323 -0
- package/__tests__/product-price-sort.ts +11 -6
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/refetch-loading-state.test.ts +158 -2
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +34 -8
- package/__tests__/starting-sort-first-page.test.ts +285 -0
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- package/dist/cjs/cache/ast.d.ts +2 -0
- package/dist/cjs/cache/ast.d.ts.map +1 -1
- package/dist/cjs/cache/ast.js +5 -2
- package/dist/cjs/cache/ast.js.map +1 -1
- package/dist/cjs/count-effect.d.ts +7 -1
- package/dist/cjs/count-effect.d.ts.map +1 -1
- package/dist/cjs/count-effect.js +15 -5
- package/dist/cjs/count-effect.js.map +1 -1
- package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
- package/dist/cjs/data-source-fetchers.js +10 -2
- package/dist/cjs/data-source-fetchers.js.map +1 -1
- package/dist/cjs/fetchers/pg-client-import.d.ts +12 -0
- package/dist/cjs/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/cjs/fetchers/pg-client-import.js +15 -0
- package/dist/cjs/fetchers/pg-client-import.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +3 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/raw-query.d.ts.map +1 -1
- package/dist/cjs/fetchers/raw-query.js +2 -1
- package/dist/cjs/fetchers/raw-query.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +4 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +14 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +89 -4
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +243 -33
- package/dist/cjs/infinite-scroll.js.map +1 -1
- package/dist/cjs/list-sort-order.d.ts +14 -0
- package/dist/cjs/list-sort-order.d.ts.map +1 -0
- package/dist/cjs/list-sort-order.js +36 -0
- package/dist/cjs/list-sort-order.js.map +1 -0
- package/dist/cjs/loading-state.d.ts +23 -2
- package/dist/cjs/loading-state.d.ts.map +1 -1
- package/dist/cjs/loading-state.js +50 -15
- package/dist/cjs/loading-state.js.map +1 -1
- package/dist/cjs/pagination-controls.d.ts +22 -3
- package/dist/cjs/pagination-controls.d.ts.map +1 -1
- package/dist/cjs/pagination-controls.js +29 -3
- package/dist/cjs/pagination-controls.js.map +1 -1
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +336 -164
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/range-control.d.ts +45 -0
- package/dist/cjs/range-control.d.ts.map +1 -0
- package/dist/cjs/range-control.js +165 -0
- package/dist/cjs/range-control.js.map +1 -0
- package/dist/cjs/row-order.d.ts +3 -2
- package/dist/cjs/row-order.d.ts.map +1 -1
- package/dist/cjs/row-order.js +5 -4
- package/dist/cjs/row-order.js.map +1 -1
- package/dist/cjs/search-input.d.ts +42 -0
- package/dist/cjs/search-input.d.ts.map +1 -0
- package/dist/cjs/search-input.js +158 -0
- package/dist/cjs/search-input.js.map +1 -0
- package/dist/cjs/sort-utils.d.ts +53 -1
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +105 -13
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/booking-item.d.ts +24 -0
- package/dist/cjs/transformations/booking-item.d.ts.map +1 -0
- package/dist/cjs/transformations/booking-item.js +27 -0
- package/dist/cjs/transformations/booking-item.js.map +1 -0
- package/dist/cjs/transformations/index.d.ts +4 -2
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +14 -5
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts +3 -2
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +48 -18
- package/dist/cjs/utils.js.map +1 -1
- package/dist/esm/cache/ast.d.ts +2 -0
- package/dist/esm/cache/ast.d.ts.map +1 -1
- package/dist/esm/cache/ast.js +3 -1
- package/dist/esm/cache/ast.js.map +1 -1
- package/dist/esm/count-effect.d.ts +7 -1
- package/dist/esm/count-effect.d.ts.map +1 -1
- package/dist/esm/count-effect.js +15 -5
- package/dist/esm/count-effect.js.map +1 -1
- package/dist/esm/data-source-fetchers.d.ts.map +1 -1
- package/dist/esm/data-source-fetchers.js +10 -2
- package/dist/esm/data-source-fetchers.js.map +1 -1
- package/dist/esm/fetchers/pg-client-import.d.ts +12 -0
- package/dist/esm/fetchers/pg-client-import.d.ts.map +1 -0
- package/dist/esm/fetchers/pg-client-import.js +12 -0
- package/dist/esm/fetchers/pg-client-import.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +3 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/raw-query.d.ts.map +1 -1
- package/dist/esm/fetchers/raw-query.js +2 -1
- package/dist/esm/fetchers/raw-query.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +4 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +89 -4
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +237 -32
- package/dist/esm/infinite-scroll.js.map +1 -1
- package/dist/esm/list-sort-order.d.ts +14 -0
- package/dist/esm/list-sort-order.d.ts.map +1 -0
- package/dist/esm/list-sort-order.js +32 -0
- package/dist/esm/list-sort-order.js.map +1 -0
- package/dist/esm/loading-state.d.ts +23 -2
- package/dist/esm/loading-state.d.ts.map +1 -1
- package/dist/esm/loading-state.js +50 -15
- package/dist/esm/loading-state.js.map +1 -1
- package/dist/esm/pagination-controls.d.ts +22 -3
- package/dist/esm/pagination-controls.d.ts.map +1 -1
- package/dist/esm/pagination-controls.js +28 -2
- package/dist/esm/pagination-controls.js.map +1 -1
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +340 -168
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/range-control.d.ts +45 -0
- package/dist/esm/range-control.d.ts.map +1 -0
- package/dist/esm/range-control.js +137 -0
- package/dist/esm/range-control.js.map +1 -0
- package/dist/esm/row-order.d.ts +3 -2
- package/dist/esm/row-order.d.ts.map +1 -1
- package/dist/esm/row-order.js +5 -4
- package/dist/esm/row-order.js.map +1 -1
- package/dist/esm/search-input.d.ts +42 -0
- package/dist/esm/search-input.d.ts.map +1 -0
- package/dist/esm/search-input.js +129 -0
- package/dist/esm/search-input.js.map +1 -0
- package/dist/esm/sort-utils.d.ts +53 -1
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +100 -12
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/booking-item.d.ts +24 -0
- package/dist/esm/transformations/booking-item.d.ts.map +1 -0
- package/dist/esm/transformations/booking-item.js +23 -0
- package/dist/esm/transformations/booking-item.js.map +1 -0
- package/dist/esm/transformations/index.d.ts +4 -2
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +14 -5
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts +3 -2
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +49 -19
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/cache/ast.ts +4 -1
- package/src/count-effect.ts +31 -8
- package/src/data-source-fetchers.ts +33 -14
- package/src/fetchers/pg-client-import.ts +16 -0
- package/src/fetchers/postgresql.ts +15 -9
- package/src/fetchers/raw-query.ts +2 -1
- package/src/fetchers/teleport.ts +19 -10
- package/src/fetchers/utils/browser-row-policy.ts +79 -5
- package/src/index.ts +8 -2
- package/src/infinite-scroll.ts +420 -81
- package/src/list-sort-order.ts +34 -0
- package/src/loading-state.ts +85 -13
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +532 -218
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +5 -4
- package/src/search-input.ts +190 -0
- package/src/sort-utils.ts +164 -21
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/index.ts +25 -2
- 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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
485
|
-
expect(code).toContain('
|
|
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
|
|