@teleporthq/teleport-plugin-next-data-source 0.44.0 → 0.44.4
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 +67 -1
- package/__tests__/browser-row-policy.test.ts +202 -2
- package/__tests__/data-source-request-locale.test.ts +4 -3
- package/__tests__/fetchers.test.ts +6 -2
- package/__tests__/infinite-scroll.test.ts +210 -5
- package/__tests__/page-data-visitor-read.test.ts +196 -0
- package/__tests__/plain-list-sort.test.ts +1 -1
- package/__tests__/plain-list-starting-sort.test.ts +323 -0
- package/__tests__/refetch-loading-state.test.ts +158 -2
- package/__tests__/starting-sort-first-page.test.ts +285 -0
- 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/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/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +1 -1
- 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 +64 -2
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +102 -3
- package/dist/cjs/infinite-scroll.js.map +1 -1
- 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-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +200 -64
- package/dist/cjs/pagination-plugin.js.map +1 -1
- 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/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/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/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +1 -1
- 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 +64 -2
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +97 -2
- package/dist/esm/infinite-scroll.js.map +1 -1
- 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-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +204 -68
- package/dist/esm/pagination-plugin.js.map +1 -1
- 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/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 +4 -4
- package/src/cache/ast.ts +4 -1
- package/src/data-source-fetchers.ts +33 -14
- package/src/fetchers/teleport.ts +8 -2
- package/src/fetchers/utils/browser-row-policy.ts +79 -5
- package/src/index.ts +8 -2
- package/src/infinite-scroll.ts +182 -4
- package/src/loading-state.ts +85 -13
- package/src/pagination-plugin.ts +369 -83
- package/src/sort-utils.ts +164 -21
- package/src/utils.ts +55 -18
|
@@ -6,9 +6,11 @@ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
|
6
6
|
import {
|
|
7
7
|
buildAccumulatingRequestMark,
|
|
8
8
|
buildAccumulatingResponseHandler,
|
|
9
|
+
buildFirstPageSeedEffect,
|
|
9
10
|
getInfiniteScrollVars,
|
|
10
11
|
} from '../src/infinite-scroll'
|
|
11
12
|
import {
|
|
13
|
+
makeGetStaticPropsChunk,
|
|
12
14
|
makeMultiMapperUidl,
|
|
13
15
|
makeUidlNode,
|
|
14
16
|
makeWidgetComponentChunk,
|
|
@@ -63,14 +65,18 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
63
65
|
)
|
|
64
66
|
})
|
|
65
67
|
|
|
66
|
-
it('keeps the page out of the provider key and
|
|
68
|
+
it('keeps the page out of the provider key, and a component fetches its own page 1', async () => {
|
|
67
69
|
const code = await runPlugin({ infiniteScroll: true })
|
|
68
70
|
|
|
69
|
-
// Remounting per appended page would throw away the accumulated rows
|
|
71
|
+
// Remounting per appended page would throw away the accumulated rows.
|
|
70
72
|
expect(code).not.toContain('ds_0_page}`')
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
+
// A component has no `getStaticProps` rows to start from, so its provider
|
|
74
|
+
// fetches page 1 — and that answer fills the accumulator. This used to hold
|
|
75
|
+
// for every appending list, pages included: the server HTML then carried
|
|
76
|
+
// the loading state although the page's props held the rows. A page now
|
|
77
|
+
// starts from them (next describe).
|
|
73
78
|
expect(code).not.toContain('initialData')
|
|
79
|
+
expect(code).not.toContain('ds_0_firstPage')
|
|
74
80
|
})
|
|
75
81
|
|
|
76
82
|
it('observes a sentinel after the list when there is no Load More button', async () => {
|
|
@@ -179,6 +185,139 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
179
185
|
})
|
|
180
186
|
})
|
|
181
187
|
|
|
188
|
+
/**
|
|
189
|
+
* On a page, `getStaticProps` has already fetched the list's first page. The
|
|
190
|
+
* list starts from it — in the server HTML, and as page 1 of the accumulator —
|
|
191
|
+
* instead of painting its loading state and fetching the same rows again.
|
|
192
|
+
*/
|
|
193
|
+
describe('pagination plugin — infinite scroll on a page', () => {
|
|
194
|
+
const runPagePlugin = async (
|
|
195
|
+
repeater: RepeaterOptions
|
|
196
|
+
): Promise<{ component: string; staticProps: string }> => {
|
|
197
|
+
const chunk = makeWidgetComponentChunk({ controls: ['load-more'] })
|
|
198
|
+
const staticProps = makeGetStaticPropsChunk()
|
|
199
|
+
const structure: ComponentStructure = {
|
|
200
|
+
uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
|
|
201
|
+
chunks: [chunk, staticProps],
|
|
202
|
+
dependencies: {},
|
|
203
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
204
|
+
} as never
|
|
205
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
206
|
+
return {
|
|
207
|
+
component: collapse(generator(chunk.content as types.Node).code),
|
|
208
|
+
staticProps: collapse(generator(staticProps.content as types.Node).code),
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const stateFilter = {
|
|
213
|
+
type: 'condition',
|
|
214
|
+
source: 'category',
|
|
215
|
+
destination: { type: 'dynamic', content: { referenceType: 'state', id: 'selectedCategory' } },
|
|
216
|
+
operand: '=',
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
it('starts from the rows its page fetched, frozen for the life of the page', async () => {
|
|
220
|
+
const { component, staticProps } = await runPagePlugin({ infiniteScroll: true })
|
|
221
|
+
|
|
222
|
+
// The rows the page's getStaticProps fetched for this list...
|
|
223
|
+
expect(staticProps).toContain('items_ds_0: items_ds_0')
|
|
224
|
+
// ...are what the provider starts from, so the server HTML holds them. The
|
|
225
|
+
// value never moves with the page: the provider copies a changed
|
|
226
|
+
// `initialData` over what it shows, which would wipe the appended rows.
|
|
227
|
+
expect(component).toContain('initialData={ds_0_firstPage}')
|
|
228
|
+
expect(component).toContain(
|
|
229
|
+
'const ds_0_firstPage = useMemo(() => props?.items_ds_0?.length > 0 ? props?.items_ds_0 : undefined, [])'
|
|
230
|
+
)
|
|
231
|
+
expect(component).not.toMatch(/initialData=\{ds_0_page === 1/)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('files the rows as page 1 of the accumulator, under the signature fetchData computes', async () => {
|
|
235
|
+
const { component } = await runPagePlugin({ infiniteScroll: true })
|
|
236
|
+
|
|
237
|
+
// One params object for the provider and the signature.
|
|
238
|
+
expect(component).toContain(
|
|
239
|
+
'const ds_0_params = useMemo(() => ({ page: ds_0_page, perPage: 20 }), [ds_0_page])'
|
|
240
|
+
)
|
|
241
|
+
expect(component).toContain('params={ds_0_params}')
|
|
242
|
+
expect(component).toContain(
|
|
243
|
+
'useEffect(() => { if (ds_0_firstPage === undefined) return; const rows = ds_0_firstPage || []; const sig = JSON.stringify({ ...ds_0_params, page: 0 }); ds_0_accumRef.current = { sig, pages: { 1: rows }, latest: { sig, page: 1 } }; setDs_0_hasMoreFlag(rows.length >= 20); }, [ds_0_firstPage]);'
|
|
244
|
+
)
|
|
245
|
+
// Declared before anything reads them.
|
|
246
|
+
expect(component.indexOf('const ds_0_params =')).toBeLessThan(
|
|
247
|
+
component.indexOf('const ds_0_firstPage =')
|
|
248
|
+
)
|
|
249
|
+
expect(component.indexOf('const ds_0_firstPage =')).toBeLessThan(
|
|
250
|
+
component.indexOf('if (ds_0_firstPage === undefined)')
|
|
251
|
+
)
|
|
252
|
+
expect(component.indexOf('const ds_0_accumRef =')).toBeLessThan(
|
|
253
|
+
component.indexOf('if (ds_0_firstPage === undefined)')
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('takes the rows only while no search is applied, frozen per query', async () => {
|
|
258
|
+
const { component } = await runPagePlugin({ infiniteScroll: true, searchEnabled: true })
|
|
259
|
+
|
|
260
|
+
// The provider remounts per query (its key); the first page is decided once
|
|
261
|
+
// per mount, with the query and filters as they stand then.
|
|
262
|
+
// tslint:disable-next-line:no-invalid-template-strings
|
|
263
|
+
expect(component).toContain('key={`items-${ds_0_state.debouncedQuery}`}')
|
|
264
|
+
expect(component).toContain(
|
|
265
|
+
'const ds_0_firstPage = useMemo(() => !ds_0_state.debouncedQuery && props?.items_ds_0?.length > 0 ? props?.items_ds_0 : undefined, [ds_0_state.debouncedQuery])'
|
|
266
|
+
)
|
|
267
|
+
expect(component).toContain('initialData={ds_0_firstPage}')
|
|
268
|
+
expect(component).toContain('const sig = JSON.stringify({ ...ds_0_params, page: 0 })')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('does not take them while a filter is set', async () => {
|
|
272
|
+
const { component } = await runPagePlugin({ infiniteScroll: true, filters: [stateFilter] })
|
|
273
|
+
|
|
274
|
+
// The build-time fetch knows no visitor's filter: the same guard as the
|
|
275
|
+
// paged lists.
|
|
276
|
+
expect(component).toContain(
|
|
277
|
+
'const ds_0_firstPage = useMemo(() => !selectedCategory && props?.items_ds_0?.length > 0 ? props?.items_ds_0 : undefined, [])'
|
|
278
|
+
)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('reads the rows inside a payload that wraps them', async () => {
|
|
282
|
+
const { component } = await runPagePlugin({
|
|
283
|
+
infiniteScroll: true,
|
|
284
|
+
dataSourceType: 'rest-api',
|
|
285
|
+
source: 'items?.templates || []',
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
expect(component).toContain(
|
|
289
|
+
'useMemo(() => props?.items_ds_0?.templates?.length > 0 ? props?.items_ds_0 : undefined, [])'
|
|
290
|
+
)
|
|
291
|
+
expect(component).toContain('const rows = ds_0_firstPage?.templates || [];')
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('reuses the params a cached list already hoisted', async () => {
|
|
295
|
+
const { component } = await runPagePlugin({
|
|
296
|
+
infiniteScroll: true,
|
|
297
|
+
cache: { enabled: true, ttlSeconds: 300, client: true, server: true },
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
expect(component.match(/const ds_0_params = /g)).toHaveLength(1)
|
|
301
|
+
expect(component).toContain('initialData={ds_0_firstPage}')
|
|
302
|
+
expect(component).toContain('const sig = JSON.stringify({ ...ds_0_params, page: 0 })')
|
|
303
|
+
expect(component.indexOf('const ds_0_params =')).toBeLessThan(
|
|
304
|
+
component.indexOf('if (ds_0_firstPage === undefined)')
|
|
305
|
+
)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('keeps fetching page 1 in the browser under a sort the visitor picks', async () => {
|
|
309
|
+
const { component } = await runPagePlugin({
|
|
310
|
+
infiniteScroll: true,
|
|
311
|
+
sort: { type: 'expr', content: 'sortBy.split("-")[0]' },
|
|
312
|
+
sortDirection: { type: 'expr', content: 'sortBy.split("-")[1]' },
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// The build-time fetch has no sort, so its rows are not this list's page 1.
|
|
316
|
+
expect(component).not.toContain('initialData')
|
|
317
|
+
expect(component).not.toContain('ds_0_firstPage')
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
182
321
|
/**
|
|
183
322
|
* The accumulator, executed: `fetchData` as the page emits it, answered in
|
|
184
323
|
* the orders a browser can produce. A sort (filter, search) change made on
|
|
@@ -224,7 +363,25 @@ describe('the appending list, run', () => {
|
|
|
224
363
|
made.result = result
|
|
225
364
|
return made
|
|
226
365
|
}
|
|
227
|
-
|
|
366
|
+
// The page's own first page, handed to the provider instead of fetched: the
|
|
367
|
+
// effect as the page emits it, run the way React runs it after the mount.
|
|
368
|
+
const startFromServerRows = (rows: string[], params: Deferred['params']) =>
|
|
369
|
+
new Function(
|
|
370
|
+
'useEffect',
|
|
371
|
+
'ds_0_firstPage',
|
|
372
|
+
'ds_0_params',
|
|
373
|
+
'ds_0_accumRef',
|
|
374
|
+
'setDs_0_hasMoreFlag',
|
|
375
|
+
generator(types.program([buildFirstPageSeedEffect(vars, 'ds_0_params', PER_PAGE)])).code
|
|
376
|
+
)(
|
|
377
|
+
(effect: () => void) => effect(),
|
|
378
|
+
rows,
|
|
379
|
+
params,
|
|
380
|
+
accumRef,
|
|
381
|
+
(value: boolean) => hasMore.push(value)
|
|
382
|
+
)
|
|
383
|
+
const requestedPages = () => requests.map((made) => `${made.params.sorts}${made.params.page}`)
|
|
384
|
+
return { request, hasMore, startFromServerRows, requestedPages }
|
|
228
385
|
}
|
|
229
386
|
|
|
230
387
|
const rowsOf = (sorts: string, page: number) => [`${sorts}${page}a`, `${sorts}${page}b`]
|
|
@@ -275,6 +432,54 @@ describe('the appending list, run', () => {
|
|
|
275
432
|
expect(await before.result).toEqual(rowsOf('B', 1))
|
|
276
433
|
})
|
|
277
434
|
|
|
435
|
+
it('appends page 2 to the rows the page painted, without requesting page 1', async () => {
|
|
436
|
+
const list = bootList()
|
|
437
|
+
list.startFromServerRows(rowsOf('A', 1), { page: 1, sorts: 'A' })
|
|
438
|
+
expect(list.hasMore).toEqual([true])
|
|
439
|
+
|
|
440
|
+
const next = list.request(2, 'A')
|
|
441
|
+
next.resolve(rowsOf('A', 2))
|
|
442
|
+
expect(await next.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2)])
|
|
443
|
+
const third = list.request(3, 'A')
|
|
444
|
+
third.resolve(['A3a'])
|
|
445
|
+
expect(await third.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2), 'A3a'])
|
|
446
|
+
|
|
447
|
+
expect(list.requestedPages()).toEqual(['A2', 'A3'])
|
|
448
|
+
expect(list.hasMore).toEqual([true, true, false])
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('still starts a fresh list on a filter, sort or search change after it', async () => {
|
|
452
|
+
const list = bootList()
|
|
453
|
+
list.startFromServerRows(rowsOf('A', 1), { page: 1, sorts: 'A' })
|
|
454
|
+
|
|
455
|
+
const restart = list.request(1, 'B')
|
|
456
|
+
restart.resolve(rowsOf('B', 1))
|
|
457
|
+
expect(await restart.result).toEqual(rowsOf('B', 1))
|
|
458
|
+
const next = list.request(2, 'B')
|
|
459
|
+
next.resolve(rowsOf('B', 2))
|
|
460
|
+
expect(await next.result).toEqual([...rowsOf('B', 1), ...rowsOf('B', 2)])
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
it('ends the list when the page painted a short first page', () => {
|
|
464
|
+
const list = bootList()
|
|
465
|
+
list.startFromServerRows(['A1a'], { page: 1, sorts: 'A' })
|
|
466
|
+
expect(list.hasMore).toEqual([false])
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it('keeps the painted rows when an earlier query answers after them', async () => {
|
|
470
|
+
// A search cleared while its answer is still on the way: the provider
|
|
471
|
+
// remounts on the page's rows, then the search's page 1 lands.
|
|
472
|
+
const list = bootList()
|
|
473
|
+
const stray = list.request(1, 'B')
|
|
474
|
+
list.startFromServerRows(rowsOf('A', 1), { page: 1, sorts: 'A' })
|
|
475
|
+
stray.resolve(rowsOf('B', 1))
|
|
476
|
+
expect(await stray.result).toEqual(rowsOf('A', 1))
|
|
477
|
+
|
|
478
|
+
const next = list.request(2, 'A')
|
|
479
|
+
next.resolve(rowsOf('A', 2))
|
|
480
|
+
expect(await next.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2)])
|
|
481
|
+
})
|
|
482
|
+
|
|
278
483
|
it('ends the list on a short page of the request it stands on only', async () => {
|
|
279
484
|
const list = bootList()
|
|
280
485
|
const stray = list.request(2, 'A')
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
ChunkType,
|
|
5
|
+
FileType,
|
|
6
|
+
ComponentStructure,
|
|
7
|
+
ChunkDefinition,
|
|
8
|
+
UIDLAuthentication,
|
|
9
|
+
UIDLDataSource,
|
|
10
|
+
} from '@teleporthq/teleport-types'
|
|
11
|
+
import { createNextPagesDataSourcePlugin } from '../src/index'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A page's lists are pre-rendered from rows its getStaticProps reads, and
|
|
15
|
+
* those rows reach whoever opens the page — and anyone who asks
|
|
16
|
+
* `/_next/data` for its data. They were read as the store: a store's draft
|
|
17
|
+
* and inactive products were painted on the products page, whose page 2 the
|
|
18
|
+
* visitor's own browser then fetched from the active products only. A page
|
|
19
|
+
* now reads `{ as: 'visitor' }`; only an admin page nobody but the store's
|
|
20
|
+
* trusted readers may open reads as the store.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DS_ID = 'ds1'
|
|
24
|
+
const TABLE = 'teleport_products'
|
|
25
|
+
|
|
26
|
+
const dataSource: UIDLDataSource = {
|
|
27
|
+
id: DS_ID,
|
|
28
|
+
name: 'TeleportHQ database',
|
|
29
|
+
type: 'teleport',
|
|
30
|
+
config: { selectedTables: { [TABLE]: { columns: [{ name: 'id' }, { name: 'name' }] } } },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const jsx = (name: string, attrs: types.JSXAttribute[], children: types.JSXElement[] = []) =>
|
|
34
|
+
types.jsxElement(
|
|
35
|
+
types.jsxOpeningElement(types.jsxIdentifier(name), attrs, children.length === 0),
|
|
36
|
+
children.length === 0 ? null : types.jsxClosingElement(types.jsxIdentifier(name)),
|
|
37
|
+
children,
|
|
38
|
+
children.length === 0
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const makeListProvider = (): types.JSXElement =>
|
|
42
|
+
jsx('DataProvider', [
|
|
43
|
+
types.jsxAttribute(
|
|
44
|
+
types.jsxIdentifier('name'),
|
|
45
|
+
types.jsxExpressionContainer(types.stringLiteral('items'))
|
|
46
|
+
),
|
|
47
|
+
types.jsxAttribute(
|
|
48
|
+
types.jsxIdentifier('resourceDefinition'),
|
|
49
|
+
types.jsxExpressionContainer(
|
|
50
|
+
types.objectExpression([
|
|
51
|
+
types.objectProperty(types.stringLiteral('dataSourceId'), types.stringLiteral(DS_ID)),
|
|
52
|
+
types.objectProperty(types.stringLiteral('tableName'), types.stringLiteral(TABLE)),
|
|
53
|
+
types.objectProperty(
|
|
54
|
+
types.stringLiteral('dataSourceType'),
|
|
55
|
+
types.stringLiteral('teleport')
|
|
56
|
+
),
|
|
57
|
+
])
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
types.jsxAttribute(
|
|
61
|
+
types.jsxIdentifier('renderSuccess'),
|
|
62
|
+
types.jsxExpressionContainer(
|
|
63
|
+
types.arrowFunctionExpression(
|
|
64
|
+
[types.identifier('items')],
|
|
65
|
+
jsx('Repeater', [
|
|
66
|
+
types.jsxAttribute(
|
|
67
|
+
types.jsxIdentifier('items'),
|
|
68
|
+
types.jsxExpressionContainer(types.identifier('items'))
|
|
69
|
+
),
|
|
70
|
+
types.jsxAttribute(
|
|
71
|
+
types.jsxIdentifier('renderItem'),
|
|
72
|
+
types.jsxExpressionContainer(
|
|
73
|
+
types.arrowFunctionExpression([types.identifier('product')], jsx('div', []))
|
|
74
|
+
)
|
|
75
|
+
),
|
|
76
|
+
])
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
// tslint:disable-next-line:no-any
|
|
83
|
+
const makeListNode = (paginated: boolean): any => ({
|
|
84
|
+
type: 'data-source-list',
|
|
85
|
+
content: {
|
|
86
|
+
renderPropIdentifier: 'items',
|
|
87
|
+
resourceDefinition: { dataSourceId: DS_ID, tableName: TABLE, dataSourceType: 'teleport' },
|
|
88
|
+
resource: { id: 'TQ_products', params: {} },
|
|
89
|
+
nodes: {
|
|
90
|
+
success: {
|
|
91
|
+
type: 'cms-list-repeater',
|
|
92
|
+
content: {
|
|
93
|
+
renderPropIdentifier: 'product',
|
|
94
|
+
paginated,
|
|
95
|
+
perPage: 20,
|
|
96
|
+
searchEnabled: false,
|
|
97
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const protection = (route: string, allowedRoles: string[]) => ({
|
|
105
|
+
requiresAuth: true,
|
|
106
|
+
allowedRoles,
|
|
107
|
+
pageName: route.split('/').pop(),
|
|
108
|
+
route,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// An admin panel admitting admins, with one screen staff may open too.
|
|
112
|
+
const AUTH = {
|
|
113
|
+
enabled: true,
|
|
114
|
+
roles: ['admin', 'staff', 'user'],
|
|
115
|
+
pageProtection: {
|
|
116
|
+
'p-products': protection('/admin/products', ['admin']),
|
|
117
|
+
'p-dashboard': protection('/admin/dashboard', ['admin', 'staff']),
|
|
118
|
+
},
|
|
119
|
+
folderProtection: {
|
|
120
|
+
'f-admin': {
|
|
121
|
+
requiresAuth: true,
|
|
122
|
+
allowedRoles: ['admin'],
|
|
123
|
+
folderName: 'admin-panel',
|
|
124
|
+
parentId: 'pages',
|
|
125
|
+
children: { 'p-products': 'page', 'p-dashboard': 'page' },
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
} as unknown as UIDLAuthentication
|
|
129
|
+
|
|
130
|
+
const runPage = async (
|
|
131
|
+
paginated: boolean,
|
|
132
|
+
outputOptions: { folderPath: string[]; fileName: string; pageId?: string }
|
|
133
|
+
): Promise<string> => {
|
|
134
|
+
const provider = makeListProvider()
|
|
135
|
+
const chunk: ChunkDefinition = {
|
|
136
|
+
name: 'jsx-component',
|
|
137
|
+
type: ChunkType.AST,
|
|
138
|
+
fileType: FileType.JS,
|
|
139
|
+
linkAfter: [],
|
|
140
|
+
content: types.variableDeclaration('const', [
|
|
141
|
+
types.variableDeclarator(
|
|
142
|
+
types.identifier('ProductsList'),
|
|
143
|
+
types.arrowFunctionExpression(
|
|
144
|
+
[types.identifier('props')],
|
|
145
|
+
types.blockStatement([types.returnStatement(jsx('div', [], [provider]))])
|
|
146
|
+
)
|
|
147
|
+
),
|
|
148
|
+
]),
|
|
149
|
+
meta: { nodesLookup: { provider } },
|
|
150
|
+
}
|
|
151
|
+
const structure: ComponentStructure = {
|
|
152
|
+
uidl: { name: 'ProductsList', node: makeListNode(paginated), outputOptions },
|
|
153
|
+
chunks: [chunk],
|
|
154
|
+
dependencies: {},
|
|
155
|
+
options: { dataSources: { [DS_ID]: dataSource }, extractedResources: {}, auth: AUTH },
|
|
156
|
+
} as never
|
|
157
|
+
await createNextPagesDataSourcePlugin()(structure)
|
|
158
|
+
const getStaticProps = structure.chunks.find((c) => c.name === 'getStaticProps')
|
|
159
|
+
return generator(getStaticProps!.content as types.Node).code.replace(/\s+/g, ' ')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
describe("a page's own data", () => {
|
|
163
|
+
it('reads a paginated list page as a visitor: the first page and the count', async () => {
|
|
164
|
+
const staticProps = await runPage(true, { folderPath: [], fileName: 'products' })
|
|
165
|
+
|
|
166
|
+
expect(staticProps).toContain(
|
|
167
|
+
'teleportTeleportProductsDs1.fetchData({ page: 1, perPage: 20 }, { as: "visitor" })'
|
|
168
|
+
)
|
|
169
|
+
expect(staticProps).toContain('teleportTeleportProductsDs1.fetchCount({}, { as: "visitor" })')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('reads a plain list page as a visitor', async () => {
|
|
173
|
+
const staticProps = await runPage(false, { folderPath: [], fileName: 'products' })
|
|
174
|
+
|
|
175
|
+
expect(staticProps).toContain('teleportTeleportProductsDs1.fetchData({}, { as: "visitor" })')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('reads as the store only on an admin page nobody but its trusted readers opens', async () => {
|
|
179
|
+
const admin = await runPage(true, {
|
|
180
|
+
folderPath: ['admin'],
|
|
181
|
+
fileName: 'products',
|
|
182
|
+
pageId: 'p-products',
|
|
183
|
+
})
|
|
184
|
+
expect(admin).toContain('teleportTeleportProductsDs1.fetchData({ page: 1, perPage: 20 })')
|
|
185
|
+
expect(admin).toContain('teleportTeleportProductsDs1.fetchCount()')
|
|
186
|
+
expect(admin).not.toContain('visitor')
|
|
187
|
+
|
|
188
|
+
// Staff may open this one, and staff read what a visitor reads.
|
|
189
|
+
const staffScreen = await runPage(true, {
|
|
190
|
+
folderPath: ['admin'],
|
|
191
|
+
fileName: 'dashboard',
|
|
192
|
+
pageId: 'p-dashboard',
|
|
193
|
+
})
|
|
194
|
+
expect(staffScreen).toContain('fetchData({ page: 1, perPage: 20 }, { as: "visitor" })')
|
|
195
|
+
})
|
|
196
|
+
})
|
|
@@ -200,7 +200,7 @@ describe('a plain list fetches with its repeater’s sort', () => {
|
|
|
200
200
|
expect(code).not.toContain('field: "title"')
|
|
201
201
|
})
|
|
202
202
|
|
|
203
|
-
it('drops the prefetched rows for a sort bound to state
|
|
203
|
+
it('drops the prefetched rows for a sort bound to state whose start the build cannot know', async () => {
|
|
204
204
|
expect(squash(await runPlugin('state', { prefetched: true }))).not.toContain('initialData')
|
|
205
205
|
expect(squash(await runPlugin('fixed', { prefetched: true }))).toContain(
|
|
206
206
|
'initialData={props.items}'
|