@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
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
1
2
|
import * as types from '@babel/types'
|
|
2
3
|
import generator from '@babel/generator'
|
|
3
4
|
import { ComponentStructure } from '@teleporthq/teleport-types'
|
|
4
5
|
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
5
6
|
import {
|
|
7
|
+
buildAccumulatingRequestMark,
|
|
8
|
+
buildAccumulatingResponseHandler,
|
|
9
|
+
buildFirstPageSeedEffect,
|
|
10
|
+
getInfiniteScrollVars,
|
|
11
|
+
} from '../src/infinite-scroll'
|
|
12
|
+
import {
|
|
13
|
+
makeGetStaticPropsChunk,
|
|
6
14
|
makeMultiMapperUidl,
|
|
7
15
|
makeUidlNode,
|
|
8
16
|
makeWidgetComponentChunk,
|
|
@@ -11,9 +19,10 @@ import {
|
|
|
11
19
|
|
|
12
20
|
const runPlugin = async (
|
|
13
21
|
repeater: RepeaterOptions,
|
|
14
|
-
controls: string[] = ['previous', 'next']
|
|
22
|
+
controls: string[] = ['previous', 'next'],
|
|
23
|
+
rootTag?: string
|
|
15
24
|
): Promise<string> => {
|
|
16
|
-
const chunk = makeWidgetComponentChunk({ controls })
|
|
25
|
+
const chunk = makeWidgetComponentChunk({ controls, rootTag })
|
|
17
26
|
const structure: ComponentStructure = {
|
|
18
27
|
uidl: { name: 'TestComponent', node: makeUidlNode(repeater) },
|
|
19
28
|
chunks: [chunk],
|
|
@@ -31,7 +40,7 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
31
40
|
const code = await runPlugin({ infiniteScroll: true })
|
|
32
41
|
const flat = collapse(code)
|
|
33
42
|
|
|
34
|
-
expect(flat).toContain('const ds_0_accumRef = useRef({ sig: "", pages: {} })')
|
|
43
|
+
expect(flat).toContain('const ds_0_accumRef = useRef({ sig: "", pages: {}, latest: null })')
|
|
35
44
|
expect(flat).toContain('const [ds_0_hasMoreFlag, setDs_0_hasMoreFlag] = useState(true)')
|
|
36
45
|
// Two independent end-of-data signals: a short page, and the counted total.
|
|
37
46
|
expect(flat).toContain(
|
|
@@ -42,22 +51,32 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
42
51
|
// change must start a fresh list rather than interleave two result sets.
|
|
43
52
|
expect(flat).toContain('const sig = JSON.stringify({ ...params, page: 0 })')
|
|
44
53
|
expect(flat).toContain('if (ds_0_accumRef.current.sig !== sig)')
|
|
45
|
-
expect(flat).toContain('ds_0_accumRef.current = {
|
|
54
|
+
expect(flat).toContain('ds_0_accumRef.current.pages = {}')
|
|
46
55
|
expect(flat).toContain('ds_0_accumRef.current.pages[page] = rows')
|
|
56
|
+
// fetchData records the request the list stands on before making it.
|
|
57
|
+
expect(flat).toContain(
|
|
58
|
+
'ds_0_accumRef.current.latest = { sig: JSON.stringify({ ...params, page: 0 }), page: Number(params.page) || 1 }'
|
|
59
|
+
)
|
|
47
60
|
// A short page is the only end signal a source that cannot count gives.
|
|
48
61
|
expect(flat).toContain('setDs_0_hasMoreFlag(rows.length >= 20)')
|
|
49
62
|
// Numeric sort, so page 10 does not land between 1 and 2.
|
|
50
|
-
expect(flat).
|
|
63
|
+
expect(flat).toMatch(
|
|
64
|
+
/\.map\(Number\)\.filter\(\(?p\)? => p <= last\)\.sort\(\(a, b\) => a - b\)\.reduce\(/
|
|
65
|
+
)
|
|
51
66
|
})
|
|
52
67
|
|
|
53
|
-
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 () => {
|
|
54
69
|
const code = await runPlugin({ infiniteScroll: true })
|
|
55
70
|
|
|
56
|
-
// Remounting per appended page would throw away the accumulated rows
|
|
71
|
+
// Remounting per appended page would throw away the accumulated rows.
|
|
57
72
|
expect(code).not.toContain('ds_0_page}`')
|
|
58
|
-
//
|
|
59
|
-
//
|
|
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).
|
|
60
78
|
expect(code).not.toContain('initialData')
|
|
79
|
+
expect(code).not.toContain('ds_0_firstPage')
|
|
61
80
|
})
|
|
62
81
|
|
|
63
82
|
it('observes a sentinel after the list when there is no Load More button', async () => {
|
|
@@ -82,6 +101,30 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
82
101
|
)
|
|
83
102
|
})
|
|
84
103
|
|
|
104
|
+
it('builds the sentinel as an element its parent may hold', async () => {
|
|
105
|
+
// A <div> among a table's rows is moved out of the table by the parser:
|
|
106
|
+
// the server's markup and the client's tree then differ and hydration fails.
|
|
107
|
+
const inBody = collapse(
|
|
108
|
+
await runPlugin({ infiniteScroll: true }, ['previous', 'next'], 'tbody')
|
|
109
|
+
)
|
|
110
|
+
expect(inBody).toContain(
|
|
111
|
+
'<tr ref={ds_0_sentinelRef} aria-hidden="true" style={{ height: "1px" }}><td style={{ padding: 0, border: 0 }} /></tr>'
|
|
112
|
+
)
|
|
113
|
+
expect(inBody).not.toContain('<div ref={ds_0_sentinelRef}')
|
|
114
|
+
|
|
115
|
+
const inList = collapse(await runPlugin({ infiniteScroll: true }, ['previous', 'next'], 'ul'))
|
|
116
|
+
expect(inList).toContain(
|
|
117
|
+
'<li ref={ds_0_sentinelRef} aria-hidden="true" style={{ height: "1px", listStyle: "none" }} />'
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const inParagraph = collapse(
|
|
121
|
+
await runPlugin({ infiniteScroll: true }, ['previous', 'next'], 'p')
|
|
122
|
+
)
|
|
123
|
+
expect(inParagraph).toContain(
|
|
124
|
+
'<span ref={ds_0_sentinelRef} aria-hidden="true" style={{ display: "block", height: "1px" }} />'
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
85
128
|
it('wires the authored Load More button instead, with no observer', async () => {
|
|
86
129
|
const code = await runPlugin({ infiniteScroll: true, infiniteScrollLoadMore: true }, [
|
|
87
130
|
'load-more',
|
|
@@ -141,3 +184,314 @@ describe('pagination plugin — infinite scroll', () => {
|
|
|
141
184
|
expect(code).not.toContain('setDs_0_page(page => Math.max(1, page - 1))')
|
|
142
185
|
})
|
|
143
186
|
})
|
|
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
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The accumulator, executed: `fetchData` as the page emits it, answered in
|
|
323
|
+
* the orders a browser can produce. A sort (filter, search) change made on
|
|
324
|
+
* page 2 fires page 2 under the new query first, then page 1 once the page is
|
|
325
|
+
* reset — and `DataProvider` renders whichever answer lands last.
|
|
326
|
+
*/
|
|
327
|
+
describe('the appending list, run', () => {
|
|
328
|
+
const PER_PAGE = 2
|
|
329
|
+
|
|
330
|
+
interface Deferred {
|
|
331
|
+
params: { page: number; sorts: string }
|
|
332
|
+
resolve: (rows: string[]) => void
|
|
333
|
+
result: Promise<string[]>
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function bootList() {
|
|
337
|
+
const vars = getInfiniteScrollVars(0)
|
|
338
|
+
const mark = generator(types.program([buildAccumulatingRequestMark(vars)])).code
|
|
339
|
+
const handler = generator(buildAccumulatingResponseHandler(vars, PER_PAGE)).code
|
|
340
|
+
const accumRef = { current: { sig: '', pages: {}, latest: null } }
|
|
341
|
+
const hasMore: boolean[] = []
|
|
342
|
+
const fetchData = new Function(
|
|
343
|
+
'ds_0_accumRef',
|
|
344
|
+
'setDs_0_hasMoreFlag',
|
|
345
|
+
'answer',
|
|
346
|
+
`return function fetchData(params) { ${mark}; return answer(params).then(${handler}) }`
|
|
347
|
+
)(
|
|
348
|
+
accumRef,
|
|
349
|
+
(value: boolean) => hasMore.push(value),
|
|
350
|
+
(params: Deferred['params']) => {
|
|
351
|
+
let resolve: (rows: string[]) => void = () => undefined
|
|
352
|
+
const answered = new Promise<{ data: string[] }>((done) => {
|
|
353
|
+
resolve = (rows) => done({ data: rows })
|
|
354
|
+
})
|
|
355
|
+
requests.push({ params, resolve, result: undefined as never })
|
|
356
|
+
return answered
|
|
357
|
+
}
|
|
358
|
+
) as (params: Deferred['params']) => Promise<string[]>
|
|
359
|
+
const requests: Deferred[] = []
|
|
360
|
+
const request = (page: number, sorts: string): Deferred => {
|
|
361
|
+
const result = fetchData({ page, sorts })
|
|
362
|
+
const made = requests[requests.length - 1]
|
|
363
|
+
made.result = result
|
|
364
|
+
return made
|
|
365
|
+
}
|
|
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 }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const rowsOf = (sorts: string, page: number) => [`${sorts}${page}a`, `${sorts}${page}b`]
|
|
388
|
+
|
|
389
|
+
it('restarts at page 1 after a sort change on page 2, and loads page 2 on the next click', async () => {
|
|
390
|
+
const list = bootList()
|
|
391
|
+
const first = list.request(1, 'A')
|
|
392
|
+
first.resolve(rowsOf('A', 1))
|
|
393
|
+
const second = list.request(2, 'A')
|
|
394
|
+
second.resolve(rowsOf('A', 2))
|
|
395
|
+
expect(await second.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2)])
|
|
396
|
+
|
|
397
|
+
const stray = list.request(2, 'B')
|
|
398
|
+
const restart = list.request(1, 'B')
|
|
399
|
+
stray.resolve(rowsOf('B', 2))
|
|
400
|
+
// The stray page changes nothing: the list on show stays until page 1 answers.
|
|
401
|
+
expect(await stray.result).toEqual([...rowsOf('A', 1), ...rowsOf('A', 2)])
|
|
402
|
+
restart.resolve(rowsOf('B', 1))
|
|
403
|
+
expect(await restart.result).toEqual(rowsOf('B', 1))
|
|
404
|
+
|
|
405
|
+
const next = list.request(2, 'B')
|
|
406
|
+
next.resolve(rowsOf('B', 2))
|
|
407
|
+
expect(await next.result).toEqual([...rowsOf('B', 1), ...rowsOf('B', 2)])
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('keeps the restarted list when the stray page answers last', async () => {
|
|
411
|
+
const list = bootList()
|
|
412
|
+
list.request(1, 'A').resolve(rowsOf('A', 1))
|
|
413
|
+
const second = list.request(2, 'A')
|
|
414
|
+
second.resolve(rowsOf('A', 2))
|
|
415
|
+
await second.result
|
|
416
|
+
|
|
417
|
+
const stray = list.request(2, 'B')
|
|
418
|
+
const restart = list.request(1, 'B')
|
|
419
|
+
restart.resolve(rowsOf('B', 1))
|
|
420
|
+
expect(await restart.result).toEqual(rowsOf('B', 1))
|
|
421
|
+
stray.resolve(rowsOf('B', 2))
|
|
422
|
+
expect(await stray.result).toEqual(rowsOf('B', 1))
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('never lets a late answer to an earlier query replace the list', async () => {
|
|
426
|
+
const list = bootList()
|
|
427
|
+
const before = list.request(1, 'A')
|
|
428
|
+
const after = list.request(1, 'B')
|
|
429
|
+
after.resolve(rowsOf('B', 1))
|
|
430
|
+
expect(await after.result).toEqual(rowsOf('B', 1))
|
|
431
|
+
before.resolve(rowsOf('A', 1))
|
|
432
|
+
expect(await before.result).toEqual(rowsOf('B', 1))
|
|
433
|
+
})
|
|
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
|
+
|
|
483
|
+
it('ends the list on a short page of the request it stands on only', async () => {
|
|
484
|
+
const list = bootList()
|
|
485
|
+
const stray = list.request(2, 'A')
|
|
486
|
+
const page1 = list.request(1, 'A')
|
|
487
|
+
stray.resolve(['A2a'])
|
|
488
|
+
await stray.result
|
|
489
|
+
expect(list.hasMore).toEqual([])
|
|
490
|
+
page1.resolve(rowsOf('A', 1))
|
|
491
|
+
await page1.result
|
|
492
|
+
const page2 = list.request(2, 'A')
|
|
493
|
+
page2.resolve(['A2a'])
|
|
494
|
+
expect(await page2.result).toEqual([...rowsOf('A', 1), 'A2a'])
|
|
495
|
+
expect(list.hasMore).toEqual([true, false])
|
|
496
|
+
})
|
|
497
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import { generateDataSourceFetcher } from '../src/data-source-fetchers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How the SQL routes of the platform's listings order what a visitor's sort
|
|
6
|
+
* leaves tied or empty: a post without a reading time comes after the others
|
|
7
|
+
* whichever way the list is sorted ("longest read first" opened on them), and
|
|
8
|
+
* equal rows keep one order across pages.
|
|
9
|
+
*
|
|
10
|
+
* Pinned on the route the project generator ships (`generateDataSourceFetcher`),
|
|
11
|
+
* for both databases a blog can live in.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ROUTES = [
|
|
15
|
+
['teleport', { host: 'db', database: 'blog' }],
|
|
16
|
+
['postgresql', { connectionString: 'postgres://user:pw@host:5432/db' }],
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
const routeFor = (type: string, config: object, tableName: string): string =>
|
|
20
|
+
generateDataSourceFetcher(
|
|
21
|
+
{ type, config } as unknown as Parameters<typeof generateDataSourceFetcher>[0],
|
|
22
|
+
tableName
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const orderByLines = (route: string): string[] =>
|
|
26
|
+
route.split('\n').filter((line) => line.includes('ORDER BY') && line.includes('Clauses.join'))
|
|
27
|
+
|
|
28
|
+
describe.each(ROUTES)('the %s route of the blog listing', (type, config) => {
|
|
29
|
+
const route = routeFor(type, config, 'teleport_blog_posts')
|
|
30
|
+
|
|
31
|
+
it('parses as valid JavaScript', () => {
|
|
32
|
+
expect(() => parse(route, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('puts the rows without a value last, in both directions', () => {
|
|
36
|
+
expect(route).toMatch(/\$\{orderOf\(sort\)\} NULLS LAST`/)
|
|
37
|
+
expect(route).toMatch(/\$\{sortByDirection\} NULLS LAST\$\{tiebreak\}`/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('falls back to a plain ORDER BY when the database rejects it', () => {
|
|
41
|
+
expect(route).toMatch(/plainClauses = valid\.map\(\(sort\) => `[^`]*\$\{orderOf\(sort\)\}`\)/)
|
|
42
|
+
expect(route).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
|
|
43
|
+
expect(route).toContain('if (!usedRewrittenSort) throw sortError')
|
|
44
|
+
expect(route).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('breaks ties by id, in the ORDER BY and in its fallback', () => {
|
|
48
|
+
expect(route).toContain(`fields.includes("id") ? '' : ", \\"id\\" " + direction`)
|
|
49
|
+
const lines = orderByLines(route)
|
|
50
|
+
expect(lines).toHaveLength(2)
|
|
51
|
+
lines.forEach((line) => expect(line).toMatch(/\$\{tiebreak\}/))
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('a table no listing sorts for visitors', () => {
|
|
56
|
+
it('gets the empty-values-last order but no tiebreaker', () => {
|
|
57
|
+
const route = routeFor('teleport', { host: 'db', database: 'store' }, 'teleport_orders')
|
|
58
|
+
expect(route).toMatch(/\$\{orderOf\(sort\)\} NULLS LAST`/)
|
|
59
|
+
expect(route).toContain("const rowOrderTiebreak = () => ''")
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A board's "No status" column lists the rows whose status is empty. The GUI
|
|
6
|
+
* sends that condition as `{ operand: '=', destination: null }` (and "has a
|
|
7
|
+
* status" as `'!='` + null), the one null shape every fetcher answers with
|
|
8
|
+
* `IS NULL` / `IS NOT NULL`. An operand the fetchers do not know is dropped
|
|
9
|
+
* with its null value — and a list whose only condition was dropped reads the
|
|
10
|
+
* whole table: every row in "No status", whose move button then overwrote the
|
|
11
|
+
* real statuses. Verdicts come from the SQL the executed route sends.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface Captured {
|
|
15
|
+
sql: string
|
|
16
|
+
params: unknown[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const bootRoute = (type: 'teleport' | 'postgresql') => {
|
|
20
|
+
const dataSource = {
|
|
21
|
+
id: 'ds-1',
|
|
22
|
+
name: 'db',
|
|
23
|
+
type,
|
|
24
|
+
config:
|
|
25
|
+
type === 'teleport'
|
|
26
|
+
? {
|
|
27
|
+
host: 'teleporthq.secrets.TELEPORT_DB_HOST',
|
|
28
|
+
port: 5432,
|
|
29
|
+
user: 'teleporthq.secrets.TELEPORT_DB_USER',
|
|
30
|
+
password: 'teleporthq.secrets.TELEPORT_DB_PASSWORD',
|
|
31
|
+
database: 'teleporthq.secrets.TELEPORT_DB_NAME',
|
|
32
|
+
selectedTables: {
|
|
33
|
+
team_events: { columns: [{ name: 'id' }, { name: 'priority' }] },
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: { host: 'h', port: 5432, user: 'u', password: 'p', database: 'd' },
|
|
37
|
+
}
|
|
38
|
+
const code = generateDataSourceFetcherWithCore(dataSource as never, 'team_events', false, {})
|
|
39
|
+
.replace(
|
|
40
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
41
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
42
|
+
)
|
|
43
|
+
.replace(/^export default async function handler/m, 'async function handler')
|
|
44
|
+
.replace(/^export \{[^}]*\}\s*$/m, '')
|
|
45
|
+
.replace(/^export default \{[^}]*\}\s*$/m, '')
|
|
46
|
+
.concat('\nmodule.exports = { handler: handler }')
|
|
47
|
+
|
|
48
|
+
const captured: Captured[] = []
|
|
49
|
+
class FakeClient {
|
|
50
|
+
public async connect(): Promise<void> {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
public async end(): Promise<void> {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
public async query(sql: string, params: unknown[] = []): Promise<{ rows: any[] }> {
|
|
57
|
+
captured.push({ sql, params })
|
|
58
|
+
return { rows: [] }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const moduleObj: { exports: any } = { exports: {} }
|
|
62
|
+
new Function('require', 'module', 'exports', 'process', 'console', code)(
|
|
63
|
+
(name: string) => {
|
|
64
|
+
if (name === 'pg') {
|
|
65
|
+
return { Client: FakeClient }
|
|
66
|
+
}
|
|
67
|
+
throw new Error('unexpected require: ' + name)
|
|
68
|
+
},
|
|
69
|
+
moduleObj,
|
|
70
|
+
moduleObj.exports,
|
|
71
|
+
{ env: {} },
|
|
72
|
+
{ error: () => undefined, warn: () => undefined, log: () => undefined }
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return async (filters: unknown): Promise<Captured> => {
|
|
76
|
+
captured.length = 0
|
|
77
|
+
let status = 0
|
|
78
|
+
const res = {
|
|
79
|
+
status(statusCode: number) {
|
|
80
|
+
status = statusCode
|
|
81
|
+
return this
|
|
82
|
+
},
|
|
83
|
+
json() {
|
|
84
|
+
return this
|
|
85
|
+
},
|
|
86
|
+
setHeader() {
|
|
87
|
+
return this
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
await moduleObj.exports.handler(
|
|
91
|
+
{ method: 'GET', query: { filters: JSON.stringify(filters) }, headers: {} },
|
|
92
|
+
res
|
|
93
|
+
)
|
|
94
|
+
expect(status).toBe(200)
|
|
95
|
+
const select = captured.find((entry) => /FROM\s+"?team_events"?/i.test(entry.sql))
|
|
96
|
+
if (!select) {
|
|
97
|
+
throw new Error('the route never read team_events')
|
|
98
|
+
}
|
|
99
|
+
return select
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The domain → UIDL mapper's output for one condition in the inspector's group.
|
|
104
|
+
const condition = (operand: string, destination: unknown = null) => [
|
|
105
|
+
{
|
|
106
|
+
type: 'group',
|
|
107
|
+
operator: 'and',
|
|
108
|
+
children: [{ type: 'condition', source: 'priority', destination, operand }],
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
describe.each(['teleport', 'postgresql'] as const)('the %s read route', (type) => {
|
|
113
|
+
const read = bootRoute(type)
|
|
114
|
+
|
|
115
|
+
it('answers "is empty" (= null) with IS NULL', async () => {
|
|
116
|
+
expect((await read(condition('='))).sql).toMatch(/WHERE\s+priority IS NULL/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('answers "is not empty" (!= null) with IS NOT NULL', async () => {
|
|
120
|
+
expect((await read(condition('!='))).sql).toMatch(/WHERE\s+priority IS NOT NULL/)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('still binds a plain value as a parameter', async () => {
|
|
124
|
+
const select = await read(condition('=', 'high'))
|
|
125
|
+
expect(select.sql).toMatch(/WHERE\s+priority = \$1/)
|
|
126
|
+
expect(select.params).toEqual(expect.arrayContaining(['high']))
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('reads the whole table for an operand it does not know — why the mapper never sends one', async () => {
|
|
130
|
+
expect((await read(condition('is_null'))).sql).not.toMatch(/WHERE/)
|
|
131
|
+
})
|
|
132
|
+
})
|