@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
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor */
|
|
2
|
+
import { generateDataSourceFetcherWithCore } from '../src/data-source-fetchers'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A Postgres DATE is a calendar day. node-pg reads it as midnight in the
|
|
6
|
+
* server's time zone, so a server east of UTC served `2026-10-09` as
|
|
7
|
+
* `2026-10-08T21:00:00.000Z`: the list showed the day before, and an edit form
|
|
8
|
+
* that loaded the row saved that day back. Every Postgres fetcher keeps the
|
|
9
|
+
* day as its own text; a stand-in driver without `types` still loads.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
type ParserSpy = Array<{ oid: number; parse: (value: string) => unknown }>
|
|
13
|
+
|
|
14
|
+
const bootFetcher = (type: 'teleport' | 'postgresql', withTypes: boolean): ParserSpy => {
|
|
15
|
+
const dataSource = {
|
|
16
|
+
id: 'ds-1',
|
|
17
|
+
name: 'db',
|
|
18
|
+
type,
|
|
19
|
+
config:
|
|
20
|
+
type === 'teleport'
|
|
21
|
+
? {
|
|
22
|
+
host: 'teleporthq.secrets.TELEPORT_DB_HOST',
|
|
23
|
+
port: 5432,
|
|
24
|
+
user: 'teleporthq.secrets.TELEPORT_DB_USER',
|
|
25
|
+
password: 'teleporthq.secrets.TELEPORT_DB_PASSWORD',
|
|
26
|
+
database: 'teleporthq.secrets.TELEPORT_DB_NAME',
|
|
27
|
+
selectedTables: { orders: { columns: [{ name: 'id' }, { name: 'due' }] } },
|
|
28
|
+
}
|
|
29
|
+
: { host: 'h', port: 5432, user: 'u', password: 'p', database: 'd' },
|
|
30
|
+
}
|
|
31
|
+
const code = generateDataSourceFetcherWithCore(dataSource as never, 'orders', false, {})
|
|
32
|
+
expect(code.startsWith("import { Client, types as pgTypes } from 'pg'")).toBe(true)
|
|
33
|
+
const runnable = code
|
|
34
|
+
.replace(
|
|
35
|
+
"import { Client, types as pgTypes } from 'pg'",
|
|
36
|
+
"const { Client, types: pgTypes } = require('pg')"
|
|
37
|
+
)
|
|
38
|
+
.replace(/^export default async function handler/m, 'async function handler')
|
|
39
|
+
.replace(/^export \{[^}]*\}\s*$/m, '')
|
|
40
|
+
.replace(/^export default \{[^}]*\}\s*$/m, '')
|
|
41
|
+
const registered: ParserSpy = []
|
|
42
|
+
const types = {
|
|
43
|
+
setTypeParser: (oid: number, parse: (value: string) => unknown) =>
|
|
44
|
+
registered.push({ oid, parse }),
|
|
45
|
+
}
|
|
46
|
+
new Function('require', 'module', 'exports', 'process', 'console', runnable)(
|
|
47
|
+
(name: string) => {
|
|
48
|
+
if (name === 'pg') {
|
|
49
|
+
return withTypes ? { Client: class {}, types } : { Client: class {} }
|
|
50
|
+
}
|
|
51
|
+
throw new Error('unexpected require: ' + name)
|
|
52
|
+
},
|
|
53
|
+
{ exports: {} },
|
|
54
|
+
{},
|
|
55
|
+
{ env: {} },
|
|
56
|
+
{ error: () => undefined, warn: () => undefined, log: () => undefined }
|
|
57
|
+
)
|
|
58
|
+
return registered
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe.each(['teleport', 'postgresql'] as const)('%s fetcher DATE columns', (type) => {
|
|
62
|
+
it('keeps a DATE as the day it names', () => {
|
|
63
|
+
const registered = bootFetcher(type, true)
|
|
64
|
+
const date = registered.find(({ oid }) => oid === 1082)
|
|
65
|
+
expect(date).toBeDefined()
|
|
66
|
+
expect(date?.parse('2026-10-09')).toBe('2026-10-09')
|
|
67
|
+
expect(registered.map(({ oid }) => oid)).toEqual([1082])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('loads with a driver that has no type registry', () => {
|
|
71
|
+
expect(bootFetcher(type, false)).toEqual([])
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -182,7 +182,7 @@ describe('a plain list fetches with its repeater’s sort', () => {
|
|
|
182
182
|
it('sends a sort bound to state, and refetches when the state changes', async () => {
|
|
183
183
|
const code = squash(await runPlugin('state'))
|
|
184
184
|
expect(code).toContain(
|
|
185
|
-
'sorts: JSON.stringify([{ field: sortBy.split("-")[0], order: sortBy.split("-")[1] }])'
|
|
185
|
+
'sorts: JSON.stringify([{ field: sortBy.split("-")?.[0], order: sortBy.split("-")?.[1] }])'
|
|
186
186
|
)
|
|
187
187
|
expect(code).toMatch(/\}\), \[sortBy\]\)\}/)
|
|
188
188
|
})
|
|
@@ -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}'
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/* tslint:disable:function-constructor no-any */
|
|
2
|
+
import * as types from '@babel/types'
|
|
3
|
+
import generator from '@babel/generator'
|
|
4
|
+
import {
|
|
5
|
+
ChunkDefinition,
|
|
6
|
+
ChunkType,
|
|
7
|
+
ComponentStructure,
|
|
8
|
+
FileType,
|
|
9
|
+
UIDLStateDefinition,
|
|
10
|
+
} from '@teleporthq/teleport-types'
|
|
11
|
+
import { createNextPagesDataSourcePlugin } from '../src/index'
|
|
12
|
+
import {
|
|
13
|
+
bootProductsRoute,
|
|
14
|
+
browserParams,
|
|
15
|
+
buildTimeArguments,
|
|
16
|
+
orderBy,
|
|
17
|
+
rowWindow,
|
|
18
|
+
} from './_helpers/first-page-runtime'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A PLAIN list — no paging, no search — sorted by the dropdown a visitor picks
|
|
22
|
+
* from (`sortBy.split("-")[0]`, a page state starting at "name-asc"). The
|
|
23
|
+
* page's getStaticProps fetched its rows unsorted and the list dropped them, so
|
|
24
|
+
* the server HTML carried its loading slot and the browser fetched the rows on
|
|
25
|
+
* load. The build now fetches them by the sort a visitor starts with, the list
|
|
26
|
+
* shows them while the sort is still there, and any other sort is fetched fresh
|
|
27
|
+
* — the rule the paged lists follow (starting-sort-first-page.test.ts).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const DS_ID = 'ds1'
|
|
31
|
+
const TABLE = 'teleport_products'
|
|
32
|
+
|
|
33
|
+
const STORE_SORT = {
|
|
34
|
+
sort: { type: 'expr', content: `sortBy.split("-")[0] || ''` },
|
|
35
|
+
sortDirection: { type: 'expr', content: `sortBy.split("-")[1] || ''` },
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The sort dropdown's state, as the store template declares it.
|
|
39
|
+
const SORT_DROPDOWN: Record<string, UIDLStateDefinition> = {
|
|
40
|
+
sortBy: { type: 'string', defaultValue: 'name-asc', urlSearchParamBinding: { key: 'sortBy' } },
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const STARTING_SORTS = `(sortBy => JSON.stringify([{ field: sortBy.split("-")?.[0] || '', order: sortBy.split("-")?.[1] || '' }]))("name-asc")`
|
|
44
|
+
|
|
45
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
46
|
+
|
|
47
|
+
const jsx = (name: string, attrs: types.JSXAttribute[], children: types.JSXElement[] = []) =>
|
|
48
|
+
types.jsxElement(
|
|
49
|
+
types.jsxOpeningElement(types.jsxIdentifier(name), attrs, children.length === 0),
|
|
50
|
+
children.length === 0 ? null : types.jsxClosingElement(types.jsxIdentifier(name)),
|
|
51
|
+
children,
|
|
52
|
+
children.length === 0
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// The provider the component generator emits for a list of six products.
|
|
56
|
+
const makeProvider = (): types.JSXElement =>
|
|
57
|
+
jsx('DataProvider', [
|
|
58
|
+
types.jsxAttribute(
|
|
59
|
+
types.jsxIdentifier('name'),
|
|
60
|
+
types.jsxExpressionContainer(types.stringLiteral('items'))
|
|
61
|
+
),
|
|
62
|
+
types.jsxAttribute(
|
|
63
|
+
types.jsxIdentifier('resourceDefinition'),
|
|
64
|
+
types.jsxExpressionContainer(
|
|
65
|
+
types.objectExpression([
|
|
66
|
+
types.objectProperty(types.stringLiteral('dataSourceId'), types.stringLiteral(DS_ID)),
|
|
67
|
+
types.objectProperty(types.stringLiteral('tableName'), types.stringLiteral(TABLE)),
|
|
68
|
+
types.objectProperty(
|
|
69
|
+
types.stringLiteral('dataSourceType'),
|
|
70
|
+
types.stringLiteral('teleport')
|
|
71
|
+
),
|
|
72
|
+
])
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
types.jsxAttribute(
|
|
76
|
+
types.jsxIdentifier('params'),
|
|
77
|
+
types.jsxExpressionContainer(
|
|
78
|
+
types.objectExpression([
|
|
79
|
+
types.objectProperty(types.stringLiteral('limit'), types.numericLiteral(6)),
|
|
80
|
+
])
|
|
81
|
+
)
|
|
82
|
+
),
|
|
83
|
+
types.jsxAttribute(
|
|
84
|
+
types.jsxIdentifier('renderSuccess'),
|
|
85
|
+
types.jsxExpressionContainer(
|
|
86
|
+
types.arrowFunctionExpression(
|
|
87
|
+
[types.identifier('items')],
|
|
88
|
+
jsx('Repeater', [
|
|
89
|
+
types.jsxAttribute(
|
|
90
|
+
types.jsxIdentifier('items'),
|
|
91
|
+
types.jsxExpressionContainer(types.identifier('items'))
|
|
92
|
+
),
|
|
93
|
+
types.jsxAttribute(
|
|
94
|
+
types.jsxIdentifier('renderItem'),
|
|
95
|
+
types.jsxExpressionContainer(
|
|
96
|
+
types.arrowFunctionExpression([types.identifier('product')], jsx('div', []))
|
|
97
|
+
)
|
|
98
|
+
),
|
|
99
|
+
])
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
interface ListOptions {
|
|
106
|
+
sorted?: boolean
|
|
107
|
+
filters?: unknown[]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const makeListNode = ({ sorted = true, filters }: ListOptions = {}): any => ({
|
|
111
|
+
type: 'data-source-list',
|
|
112
|
+
content: {
|
|
113
|
+
renderPropIdentifier: 'items',
|
|
114
|
+
resourceDefinition: { dataSourceId: DS_ID, tableName: TABLE, dataSourceType: 'teleport' },
|
|
115
|
+
resource: {
|
|
116
|
+
id: 'TQ_products',
|
|
117
|
+
params: {
|
|
118
|
+
limit: { type: 'static', content: 6 },
|
|
119
|
+
...(filters ? { filters: { type: 'static', content: filters } } : {}),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
nodes: {
|
|
123
|
+
success: {
|
|
124
|
+
type: 'cms-list-repeater',
|
|
125
|
+
content: {
|
|
126
|
+
renderPropIdentifier: 'product',
|
|
127
|
+
paginated: false,
|
|
128
|
+
searchEnabled: false,
|
|
129
|
+
...(sorted ? STORE_SORT : {}),
|
|
130
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
/** A products page drawing the given plain lists, run through the page's data-source plugin. */
|
|
138
|
+
const runPage = async (
|
|
139
|
+
lists: ListOptions[],
|
|
140
|
+
// `null`: the page declares no state, so the build cannot know the sort.
|
|
141
|
+
stateDefinitions: Record<string, UIDLStateDefinition> | null = SORT_DROPDOWN
|
|
142
|
+
) => {
|
|
143
|
+
const providers = lists.map(makeProvider)
|
|
144
|
+
const chunk: ChunkDefinition = {
|
|
145
|
+
name: 'jsx-component',
|
|
146
|
+
type: ChunkType.AST,
|
|
147
|
+
fileType: FileType.JS,
|
|
148
|
+
linkAfter: [],
|
|
149
|
+
content: types.variableDeclaration('const', [
|
|
150
|
+
types.variableDeclarator(
|
|
151
|
+
types.identifier('ProductsList'),
|
|
152
|
+
types.arrowFunctionExpression(
|
|
153
|
+
[types.identifier('props')],
|
|
154
|
+
types.blockStatement([types.returnStatement(jsx('div', [], providers))])
|
|
155
|
+
)
|
|
156
|
+
),
|
|
157
|
+
]),
|
|
158
|
+
meta: { nodesLookup: Object.fromEntries(providers.map((p, i) => [`provider${i}`, p])) },
|
|
159
|
+
}
|
|
160
|
+
const structure: ComponentStructure = {
|
|
161
|
+
uidl: {
|
|
162
|
+
name: 'ProductsList',
|
|
163
|
+
node: {
|
|
164
|
+
type: 'element',
|
|
165
|
+
content: { elementType: 'container', children: lists.map(makeListNode) },
|
|
166
|
+
},
|
|
167
|
+
...(stateDefinitions ? { stateDefinitions } : {}),
|
|
168
|
+
outputOptions: { folderPath: [], fileName: 'products' },
|
|
169
|
+
},
|
|
170
|
+
chunks: [chunk],
|
|
171
|
+
dependencies: {},
|
|
172
|
+
options: {
|
|
173
|
+
dataSources: {
|
|
174
|
+
[DS_ID]: {
|
|
175
|
+
id: DS_ID,
|
|
176
|
+
name: 'TeleportHQ database',
|
|
177
|
+
type: 'teleport',
|
|
178
|
+
config: { selectedTables: { [TABLE]: { columns: [{ name: 'id' }, { name: 'name' }] } } },
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
extractedResources: {},
|
|
182
|
+
},
|
|
183
|
+
} as never
|
|
184
|
+
await createNextPagesDataSourcePlugin()(structure)
|
|
185
|
+
const staticPropsChunk = structure.chunks.find((c) => c.name === 'getStaticProps')
|
|
186
|
+
return {
|
|
187
|
+
component: collapse(generator(chunk.content as types.Node).code),
|
|
188
|
+
staticProps: staticPropsChunk
|
|
189
|
+
? collapse(generator(staticPropsChunk.content as types.Node).code)
|
|
190
|
+
: '',
|
|
191
|
+
chunk,
|
|
192
|
+
staticPropsChunk,
|
|
193
|
+
providers,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// The provider's `initialData` as the browser evaluates it for the given state.
|
|
198
|
+
const initialDataOf = (provider: types.JSXElement, state: Record<string, unknown>): unknown => {
|
|
199
|
+
const attr = provider.openingElement.attributes.find(
|
|
200
|
+
(a) => a.type === 'JSXAttribute' && a.name.name === 'initialData'
|
|
201
|
+
) as types.JSXAttribute | undefined
|
|
202
|
+
if (!attr) {
|
|
203
|
+
return undefined
|
|
204
|
+
}
|
|
205
|
+
const expression = (attr.value as types.JSXExpressionContainer).expression as types.Expression
|
|
206
|
+
const names = Object.keys(state)
|
|
207
|
+
return new Function(...names, `return (${generator(expression).code})`)(
|
|
208
|
+
...names.map((name) => state[name])
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
describe('a plain list sorted by a dropdown, on a page', () => {
|
|
213
|
+
it('fetches its rows at build time by the sort a visitor starts with, as a visitor', async () => {
|
|
214
|
+
const { staticProps } = await runPage([{}])
|
|
215
|
+
|
|
216
|
+
// The browser's own sort expression, with the dropdown at its default.
|
|
217
|
+
expect(staticProps).toContain(
|
|
218
|
+
`teleportTeleportProductsDs1.fetchData({ "limit": 6, "sorts": ${STARTING_SORTS} }, { as: "visitor" })`
|
|
219
|
+
)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('shows those rows while the sort is where a visitor starts', async () => {
|
|
223
|
+
const { component } = await runPage([{}])
|
|
224
|
+
|
|
225
|
+
expect(component).toContain(
|
|
226
|
+
'initialData={sortBy === "name-asc" ? props.items_products : undefined}'
|
|
227
|
+
)
|
|
228
|
+
// Any other sort is another query: the params follow the dropdown.
|
|
229
|
+
expect(component).toMatch(
|
|
230
|
+
/sorts: JSON\.stringify\(\[\{ field: sortBy\.split\("-"\).*\}\), \[sortBy\]\)\}/
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('keeps fetching in the browser when the build cannot know where the sort starts', async () => {
|
|
235
|
+
const { component, staticProps } = await runPage([{}], null)
|
|
236
|
+
|
|
237
|
+
expect(component).not.toContain('initialData')
|
|
238
|
+
expect(staticProps).not.toContain('"sorts"')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('fetches an unsorted list beside it with the same params on its own', async () => {
|
|
242
|
+
const { component, staticProps } = await runPage([{ sorted: false }, {}])
|
|
243
|
+
|
|
244
|
+
expect(staticProps).toContain(
|
|
245
|
+
'const [items_products, items_products_2] = await Promise.all([teleportTeleportProductsDs1.fetchData({ "limit": 6 }, { as: "visitor" })'
|
|
246
|
+
)
|
|
247
|
+
expect(staticProps).toContain(
|
|
248
|
+
`teleportTeleportProductsDs1.fetchData({ "limit": 6, "sorts": ${STARTING_SORTS} }, { as: "visitor" })`
|
|
249
|
+
)
|
|
250
|
+
expect(component).toContain('initialData={props.items_products}')
|
|
251
|
+
expect(component).toContain(
|
|
252
|
+
'initialData={sortBy === "name-asc" ? props.items_products_2 : undefined}'
|
|
253
|
+
)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('is never seeded while a visitor filters it', async () => {
|
|
257
|
+
const byCategory = {
|
|
258
|
+
type: 'condition',
|
|
259
|
+
source: 'category',
|
|
260
|
+
destination: { type: 'dynamic', content: { referenceType: 'state', id: 'selectedCategory' } },
|
|
261
|
+
operand: '=',
|
|
262
|
+
}
|
|
263
|
+
const { component, staticProps } = await runPage([{ filters: [byCategory] }], {
|
|
264
|
+
...SORT_DROPDOWN,
|
|
265
|
+
selectedCategory: { type: 'string', defaultValue: '' },
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
expect(component).not.toContain('initialData')
|
|
269
|
+
expect(staticProps).not.toContain('fetchData')
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Executed: the rows the build fetches and the ones the browser asks for must
|
|
275
|
+
* be one ordering of one row set, and the build's stand in only for the
|
|
276
|
+
* starting sort.
|
|
277
|
+
*/
|
|
278
|
+
describe('a plain list sorted by a dropdown, run', () => {
|
|
279
|
+
it('sends the build and the browser one sort string while the dropdown is untouched', async () => {
|
|
280
|
+
const { staticPropsChunk, chunk } = await runPage([{}])
|
|
281
|
+
const [atBuild, readAs] = buildTimeArguments(staticPropsChunk)
|
|
282
|
+
|
|
283
|
+
expect(readAs).toEqual({ as: 'visitor' })
|
|
284
|
+
expect(atBuild.sorts).toBe('[{"field":"name","order":"asc"}]')
|
|
285
|
+
expect(browserParams(chunk, { sortBy: 'name-asc' })).toEqual({
|
|
286
|
+
limit: '6',
|
|
287
|
+
sorts: atBuild.sorts,
|
|
288
|
+
})
|
|
289
|
+
expect(browserParams(chunk, { sortBy: 'price-desc' }).sorts).toBe(
|
|
290
|
+
'[{"field":"price","order":"desc"}]'
|
|
291
|
+
)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('orders the rows at build time exactly as the browser orders them', async () => {
|
|
295
|
+
const { staticPropsChunk, chunk } = await runPage([{}])
|
|
296
|
+
const route = bootProductsRoute()
|
|
297
|
+
|
|
298
|
+
const atBuild = await route.atBuild(buildTimeArguments(staticPropsChunk))
|
|
299
|
+
const inBrowser = await route.inBrowser(browserParams(chunk, { sortBy: 'name-asc' }))
|
|
300
|
+
|
|
301
|
+
// The same columns, rows without a value last, the row's id breaking ties,
|
|
302
|
+
// over the products a visitor is served.
|
|
303
|
+
expect(orderBy(atBuild)).toBe(' ORDER BY name ASC NULLS LAST, "id" ASC LIMIT')
|
|
304
|
+
expect(rowWindow(atBuild)).toBe(' LIMIT 6')
|
|
305
|
+
expect(atBuild).toContain("WHERE (LOWER(TRIM(status)) = 'active')")
|
|
306
|
+
expect(inBrowser).toBe(atBuild)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it("shows the build's rows for the starting sort only; any other is fetched fresh", async () => {
|
|
310
|
+
const { providers, staticPropsChunk, chunk } = await runPage([{ sorted: false }, {}])
|
|
311
|
+
const props = { items_products: ['unsorted'], items_products_2: ['by name'] }
|
|
312
|
+
|
|
313
|
+
expect(initialDataOf(providers[1], { sortBy: 'name-asc', props })).toEqual(['by name'])
|
|
314
|
+
// Another pick: no seed, and params the provider has not fetched yet.
|
|
315
|
+
expect(initialDataOf(providers[1], { sortBy: 'price-desc', props })).toBeUndefined()
|
|
316
|
+
expect(browserParams(chunk, { sortBy: 'price-desc' }, 1).sorts).not.toBe(
|
|
317
|
+
buildTimeArguments(staticPropsChunk, 'items_products_2')[0].sorts
|
|
318
|
+
)
|
|
319
|
+
// The unsorted list beside it keeps its own rows, whatever the dropdown says.
|
|
320
|
+
expect(initialDataOf(providers[0], { sortBy: 'price-desc', props })).toEqual(['unsorted'])
|
|
321
|
+
expect(buildTimeArguments(staticPropsChunk, 'items_products')[0]).toEqual({ limit: 6 })
|
|
322
|
+
})
|
|
323
|
+
})
|
|
@@ -121,8 +121,8 @@ describe('the generated Postgres route', () => {
|
|
|
121
121
|
it('keeps a plain-column ORDER BY to fall back to', () => {
|
|
122
122
|
// If a database ever rejects the sub-select, the page must degrade to the
|
|
123
123
|
// previous ordering rather than serve nothing.
|
|
124
|
-
expect(productsRoute).toContain('const
|
|
125
|
-
expect(productsRoute).toContain('if (!
|
|
124
|
+
expect(productsRoute).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
|
|
125
|
+
expect(productsRoute).toContain('if (!usedRewrittenSort) throw sortError')
|
|
126
126
|
expect(productsRoute).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
127
127
|
})
|
|
128
128
|
|
|
@@ -227,6 +227,13 @@ describe('stable pagination', () => {
|
|
|
227
227
|
expect(rowIdentityColumn({}, 'teleport_orders')).toBeNull()
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
+
it('breaks ties on every listing a visitor sorts: the blog listing and the booking page too', () => {
|
|
231
|
+
// "Sort by reading time" ties every five-minute post, "by price" every
|
|
232
|
+
// item at the same price.
|
|
233
|
+
expect(rowIdentityColumn({}, 'teleport_blog_posts')).toBe('id')
|
|
234
|
+
expect(rowIdentityColumn({}, 'teleport_booking_items')).toBe('id')
|
|
235
|
+
})
|
|
236
|
+
|
|
230
237
|
it('is applied to the real and the fallback ORDER BY, and to a single sort column', () => {
|
|
231
238
|
for (const route of [
|
|
232
239
|
generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products'),
|
|
@@ -274,10 +281,8 @@ describe('the fetcher a generated storefront actually gets', () => {
|
|
|
274
281
|
})
|
|
275
282
|
|
|
276
283
|
it('keeps the plain-column fallback and the tiebreaker', () => {
|
|
277
|
-
expect(teleportProducts).toContain(
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
expect(teleportProducts).toContain('if (!usedDiscountAwareSort) throw sortError')
|
|
284
|
+
expect(teleportProducts).toContain('const usedRewrittenSort = orderBySql !== plainOrderBySql')
|
|
285
|
+
expect(teleportProducts).toContain('if (!usedRewrittenSort) throw sortError')
|
|
281
286
|
expect(teleportProducts).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
282
287
|
expect(teleportProducts).toContain('const rowOrderTiebreak = (fields, direction) =>')
|
|
283
288
|
expect(teleportProducts).toMatch(/\$\{tiebreak\}/)
|