@teleporthq/teleport-plugin-next-data-source 0.43.57 → 0.43.60
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/pagination-widget-fixture.ts +230 -0
- package/__tests__/ecommerce-product-allow-backorders.test.ts +156 -0
- package/__tests__/ecommerce-product-discounts.test.ts +196 -0
- package/__tests__/ecommerce-product-model.test.ts +179 -0
- package/__tests__/infinite-scroll.test.ts +143 -0
- package/__tests__/numbered-pagination.test.ts +113 -0
- package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
- package/__tests__/page-url-sync.test.ts +208 -0
- package/__tests__/product-price-sort.ts +313 -0
- package/__tests__/search-url-sync.test.ts +7 -3
- package/dist/cjs/array-mapper-pagination.d.ts +9 -0
- package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
- package/dist/cjs/array-mapper-pagination.js.map +1 -1
- package/dist/cjs/count-effect.d.ts +42 -0
- package/dist/cjs/count-effect.d.ts.map +1 -0
- package/dist/cjs/count-effect.js +81 -0
- package/dist/cjs/count-effect.js.map +1 -0
- package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
- package/dist/cjs/fetchers/postgresql.js +2 -1
- package/dist/cjs/fetchers/postgresql.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +3 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +65 -0
- package/dist/cjs/infinite-scroll.d.ts.map +1 -0
- package/dist/cjs/infinite-scroll.js +190 -0
- package/dist/cjs/infinite-scroll.js.map +1 -0
- package/dist/cjs/numbered-pagination.d.ts +42 -0
- package/dist/cjs/numbered-pagination.d.ts.map +1 -0
- package/dist/cjs/numbered-pagination.js +167 -0
- package/dist/cjs/numbered-pagination.js.map +1 -0
- package/dist/cjs/page-reset.d.ts +42 -0
- package/dist/cjs/page-reset.d.ts.map +1 -0
- package/dist/cjs/page-reset.js +116 -0
- package/dist/cjs/page-reset.js.map +1 -0
- package/dist/cjs/page-url-sync.d.ts +42 -0
- package/dist/cjs/page-url-sync.d.ts.map +1 -0
- package/dist/cjs/page-url-sync.js +180 -0
- package/dist/cjs/page-url-sync.js.map +1 -0
- package/dist/cjs/pagination-controls.d.ts +55 -0
- package/dist/cjs/pagination-controls.d.ts.map +1 -0
- package/dist/cjs/pagination-controls.js +159 -0
- package/dist/cjs/pagination-controls.js.map +1 -0
- package/dist/cjs/pagination-plugin.d.ts.map +1 -1
- package/dist/cjs/pagination-plugin.js +506 -160
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +116 -0
- package/dist/cjs/product-price-sort.d.ts.map +1 -0
- package/dist/cjs/product-price-sort.js +169 -0
- package/dist/cjs/product-price-sort.js.map +1 -0
- package/dist/cjs/transformations/ecommerce-product.d.ts +12 -0
- package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +2 -1
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +2 -1
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +17 -1
- package/dist/cjs/transformations/index.js.map +1 -1
- package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
- package/dist/cjs/transformations/shared-utils.js +1 -1
- package/dist/cjs/transformations/shared-utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/array-mapper-pagination.d.ts +9 -0
- package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
- package/dist/esm/array-mapper-pagination.js.map +1 -1
- package/dist/esm/count-effect.d.ts +42 -0
- package/dist/esm/count-effect.d.ts.map +1 -0
- package/dist/esm/count-effect.js +53 -0
- package/dist/esm/count-effect.js.map +1 -0
- package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
- package/dist/esm/fetchers/postgresql.js +2 -1
- package/dist/esm/fetchers/postgresql.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +3 -2
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +65 -0
- package/dist/esm/infinite-scroll.d.ts.map +1 -0
- package/dist/esm/infinite-scroll.js +159 -0
- package/dist/esm/infinite-scroll.js.map +1 -0
- package/dist/esm/numbered-pagination.d.ts +42 -0
- package/dist/esm/numbered-pagination.d.ts.map +1 -0
- package/dist/esm/numbered-pagination.js +138 -0
- package/dist/esm/numbered-pagination.js.map +1 -0
- package/dist/esm/page-reset.d.ts +42 -0
- package/dist/esm/page-reset.d.ts.map +1 -0
- package/dist/esm/page-reset.js +87 -0
- package/dist/esm/page-reset.js.map +1 -0
- package/dist/esm/page-url-sync.d.ts +42 -0
- package/dist/esm/page-url-sync.d.ts.map +1 -0
- package/dist/esm/page-url-sync.js +152 -0
- package/dist/esm/page-url-sync.js.map +1 -0
- package/dist/esm/pagination-controls.d.ts +55 -0
- package/dist/esm/pagination-controls.d.ts.map +1 -0
- package/dist/esm/pagination-controls.js +126 -0
- package/dist/esm/pagination-controls.js.map +1 -0
- package/dist/esm/pagination-plugin.d.ts.map +1 -1
- package/dist/esm/pagination-plugin.js +507 -161
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +116 -0
- package/dist/esm/product-price-sort.d.ts.map +1 -0
- package/dist/esm/product-price-sort.js +160 -0
- package/dist/esm/product-price-sort.js.map +1 -0
- package/dist/esm/transformations/ecommerce-product.d.ts +12 -0
- package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +3 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +2 -1
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +17 -1
- package/dist/esm/transformations/index.js.map +1 -1
- package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
- package/dist/esm/transformations/shared-utils.js +1 -1
- package/dist/esm/transformations/shared-utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/array-mapper-pagination.ts +10 -0
- package/src/count-effect.ts +137 -0
- package/src/fetchers/postgresql.ts +52 -13
- package/src/fetchers/teleport.ts +53 -13
- package/src/infinite-scroll.ts +401 -0
- package/src/numbered-pagination.ts +292 -0
- package/src/page-reset.ts +162 -0
- package/src/page-url-sync.ts +241 -0
- package/src/pagination-controls.ts +176 -0
- package/src/pagination-plugin.ts +755 -346
- package/src/product-price-sort.ts +204 -0
- package/src/transformations/ecommerce-product.ts +141 -10
- package/src/transformations/index.ts +19 -1
- package/src/transformations/shared-utils.ts +14 -0
- package/dist/cjs/array-mapper-registry.d.ts +0 -85
- package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
- package/dist/cjs/array-mapper-registry.js +0 -296
- package/dist/cjs/array-mapper-registry.js.map +0 -1
- package/dist/cjs/pagination-with-count.d.ts +0 -6
- package/dist/cjs/pagination-with-count.d.ts.map +0 -1
- package/dist/cjs/pagination-with-count.js +0 -63
- package/dist/cjs/pagination-with-count.js.map +0 -1
- package/dist/esm/array-mapper-registry.d.ts +0 -85
- package/dist/esm/array-mapper-registry.d.ts.map +0 -1
- package/dist/esm/array-mapper-registry.js +0 -292
- package/dist/esm/array-mapper-registry.js.map +0 -1
- package/dist/esm/pagination-with-count.d.ts +0 -6
- package/dist/esm/pagination-with-count.d.ts.map +0 -1
- package/dist/esm/pagination-with-count.js +0 -34
- package/dist/esm/pagination-with-count.js.map +0 -1
- package/src/array-mapper-registry.ts +0 -416
- package/src/pagination-with-count.ts +0 -89
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
import generator from '@babel/generator'
|
|
3
|
+
import {
|
|
4
|
+
ChunkType,
|
|
5
|
+
FileType,
|
|
6
|
+
ComponentStructure,
|
|
7
|
+
ChunkDefinition,
|
|
8
|
+
} from '@teleporthq/teleport-types'
|
|
9
|
+
import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
|
|
10
|
+
|
|
11
|
+
const makeComponentChunk = (): ChunkDefinition => {
|
|
12
|
+
const body = types.blockStatement([
|
|
13
|
+
types.returnStatement(
|
|
14
|
+
types.jsxElement(
|
|
15
|
+
types.jsxOpeningElement(types.jsxIdentifier('div'), [], true),
|
|
16
|
+
null,
|
|
17
|
+
[],
|
|
18
|
+
true
|
|
19
|
+
)
|
|
20
|
+
),
|
|
21
|
+
])
|
|
22
|
+
const arrow = types.arrowFunctionExpression([types.identifier('props')], body)
|
|
23
|
+
return {
|
|
24
|
+
name: 'jsx-component',
|
|
25
|
+
type: ChunkType.AST,
|
|
26
|
+
fileType: FileType.JS,
|
|
27
|
+
linkAfter: [],
|
|
28
|
+
content: types.variableDeclaration('const', [
|
|
29
|
+
types.variableDeclarator(types.identifier('TestComponent'), arrow),
|
|
30
|
+
]),
|
|
31
|
+
meta: {},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ListOptions {
|
|
36
|
+
pageUrlParamKey?: string
|
|
37
|
+
searchEnabled?: boolean
|
|
38
|
+
stateFilter?: string
|
|
39
|
+
infiniteScroll?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// tslint:disable-next-line:no-any
|
|
43
|
+
const makeUidlNode = (options: ListOptions): any => ({
|
|
44
|
+
type: 'data-source-list',
|
|
45
|
+
content: {
|
|
46
|
+
renderPropIdentifier: 'items',
|
|
47
|
+
resourceDefinition: {
|
|
48
|
+
dataSourceId: 'ds1',
|
|
49
|
+
tableName: 'products',
|
|
50
|
+
dataSourceType: 'postgresql',
|
|
51
|
+
},
|
|
52
|
+
resource: {
|
|
53
|
+
params: {
|
|
54
|
+
queryColumns: { content: ['name'] },
|
|
55
|
+
...(options.stateFilter
|
|
56
|
+
? {
|
|
57
|
+
filters: {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
source: 'category_filter_ids',
|
|
61
|
+
operand: 'array_overlap',
|
|
62
|
+
destination: {
|
|
63
|
+
type: 'dynamic',
|
|
64
|
+
content: { referenceType: 'state', id: options.stateFilter },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
nodes: {
|
|
74
|
+
success: {
|
|
75
|
+
type: 'cms-list-repeater',
|
|
76
|
+
content: {
|
|
77
|
+
renderPropIdentifier: 'product',
|
|
78
|
+
paginated: true,
|
|
79
|
+
perPage: 20,
|
|
80
|
+
searchEnabled: options.searchEnabled !== false,
|
|
81
|
+
searchDebounce: 300,
|
|
82
|
+
...(options.pageUrlParamKey ? { pageUrlParamKey: options.pageUrlParamKey } : {}),
|
|
83
|
+
...(options.infiniteScroll ? { infiniteScroll: true } : {}),
|
|
84
|
+
nodes: { list: { type: 'element', content: { elementType: 'div' } } },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const runPlugin = async (options: ListOptions): Promise<string> => {
|
|
92
|
+
const chunk = makeComponentChunk()
|
|
93
|
+
const structure: ComponentStructure = {
|
|
94
|
+
uidl: { name: 'TestComponent', node: makeUidlNode(options) },
|
|
95
|
+
chunks: [chunk],
|
|
96
|
+
dependencies: {},
|
|
97
|
+
options: { dataSources: {}, extractedResources: {} },
|
|
98
|
+
} as never
|
|
99
|
+
await createNextArrayMapperPaginationPlugin()(structure)
|
|
100
|
+
return generator(chunk.content as types.Node).code
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const collapse = (code: string): string => code.replace(/\s+/g, ' ')
|
|
104
|
+
|
|
105
|
+
describe('pagination plugin — current page ⇄ URL (pageUrlParamKey)', () => {
|
|
106
|
+
it('adopts the URL page in an effect rather than seeding the state', async () => {
|
|
107
|
+
const code = await runPlugin({ pageUrlParamKey: 'page' })
|
|
108
|
+
const flat = collapse(code)
|
|
109
|
+
|
|
110
|
+
// ⛔ NOT seeded in the useState initializer, unlike the search query.
|
|
111
|
+
// A statically generated page renders page 1 on the server, and React does
|
|
112
|
+
// not repair mismatched ATTRIBUTES when it hydrates — a client that started
|
|
113
|
+
// on page 3 would keep the server's disabled "Previous" for good. Moving
|
|
114
|
+
// the page in an effect makes it a real state transition React will paint.
|
|
115
|
+
expect(code).not.toContain('window.location.search')
|
|
116
|
+
expect(flat).toContain('const [ds_0_state, setDs_0_state] = useState({ page: 1,')
|
|
117
|
+
|
|
118
|
+
expect(code).toContain('const router = useRouter()')
|
|
119
|
+
expect(code).toContain('const ds_0_pageUrlRef = useRef(null)')
|
|
120
|
+
expect(code).toContain('const __urlPage = Math.max(1, parseInt(__raw, 10) || 1)')
|
|
121
|
+
expect(code).toContain('const __statePage = ds_0_state.page')
|
|
122
|
+
expect(code).toContain('const __seenUrlPage = ds_0_pageUrlRef.current')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('decides which side moved, so the two directions cannot race', async () => {
|
|
126
|
+
const code = await runPlugin({ pageUrlParamKey: 'page' })
|
|
127
|
+
const flat = collapse(code)
|
|
128
|
+
|
|
129
|
+
// The URL moved (deep link, <Link>, back/forward) ⇒ adopt it — but ONLY
|
|
130
|
+
// once every replace this component issued has landed. Mid-flight,
|
|
131
|
+
// `router.query` still holds the page the visitor is on their way out of.
|
|
132
|
+
expect(flat).toContain(
|
|
133
|
+
'if (__tqQuerySyncRef.current.inFlight === 0 && __seenUrlPage !== __urlPage && __urlPage !== __statePage)'
|
|
134
|
+
)
|
|
135
|
+
expect(flat).toContain(
|
|
136
|
+
'setDs_0_state(state => state.page === __urlPage ? state : { ...state, page: __urlPage })'
|
|
137
|
+
)
|
|
138
|
+
// The page moved (a click, or the filter reset) ⇒ write it. Page 1 is the
|
|
139
|
+
// ABSENCE of the key, so an unpaginated visit keeps a clean URL.
|
|
140
|
+
expect(flat).toContain('if (__urlPage === __statePage) return')
|
|
141
|
+
expect(flat).toContain(
|
|
142
|
+
'__tqWriteQueryParam("page", __statePage <= 1 ? undefined : __statePage)'
|
|
143
|
+
)
|
|
144
|
+
expect(code).toContain('}, [router.query.page, router.isReady, ds_0_state.page])')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('applies to the standalone page state for a paginated-only list', async () => {
|
|
148
|
+
const code = await runPlugin({ pageUrlParamKey: 'page', searchEnabled: false })
|
|
149
|
+
const flat = collapse(code)
|
|
150
|
+
|
|
151
|
+
expect(code).toContain('const __statePage = ds_0_page')
|
|
152
|
+
expect(flat).toContain('setDs_0_page(page => page === __urlPage ? page : __urlPage)')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('runs the URL sync AFTER the filter reset so a deep link wins', async () => {
|
|
156
|
+
const code = await runPlugin({ pageUrlParamKey: 'page', stateFilter: 'selectedCategory' })
|
|
157
|
+
|
|
158
|
+
// Both effects can fire in one flush on a navigation that changes filter and
|
|
159
|
+
// page together. Effects run in emission order, so the URL sync must be last
|
|
160
|
+
// or `?page=3&categoryFilter=X` would land on page 1.
|
|
161
|
+
const resetAt = code.indexOf('ds_0_filterResetRef.current = __sig')
|
|
162
|
+
const syncAt = code.indexOf('const __seenUrlPage = ds_0_pageUrlRef.current')
|
|
163
|
+
expect(resetAt).toBeGreaterThan(-1)
|
|
164
|
+
expect(syncAt).toBeGreaterThan(resetAt)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('writes through the shared writer, so two controls cannot erase each other', async () => {
|
|
168
|
+
const code = await runPlugin({ pageUrlParamKey: 'page', stateFilter: 'selectedCategory' })
|
|
169
|
+
const flat = collapse(code)
|
|
170
|
+
|
|
171
|
+
// ⛔ THE DEFECT THIS GUARDS, seen live: picking a category while on page 3
|
|
172
|
+
// resets the page, so the category write-back and the page write-back fire
|
|
173
|
+
// in the SAME flush. Each used to build its next query from `router.query`,
|
|
174
|
+
// which Next updates asynchronously and does NOT refresh on a component's
|
|
175
|
+
// own setState — so the second `replace` wrote a query that predated the
|
|
176
|
+
// first, and `?categoryFilter=…` vanished even though the list was filtered.
|
|
177
|
+
// The read-back then cleared the category state, the write-back re-asserted
|
|
178
|
+
// it, and the pair swapped values forever: an endless refetch loop.
|
|
179
|
+
//
|
|
180
|
+
// The fix is that neither effect composes a query any more. Both hand one
|
|
181
|
+
// key to `__tqWriteQueryParam`, which merges into the last query the
|
|
182
|
+
// component ASKED for, so the second write builds on the first's payload.
|
|
183
|
+
expect(code).not.toContain('__nextQuery')
|
|
184
|
+
expect(flat).toContain('const base = __tqQuerySyncRef.current.pending || router.query')
|
|
185
|
+
expect(flat).toContain('__tqQuerySyncRef.current.pending = next')
|
|
186
|
+
|
|
187
|
+
// Exactly ONE writer is declared however many controls are bound to the URL.
|
|
188
|
+
expect((code.match(/const __tqWriteQueryParam = /g) || []).length).toBe(1)
|
|
189
|
+
// ...and exactly one `router.replace`, inside it.
|
|
190
|
+
expect((code.match(/router\.replace\(/g) || []).length).toBe(1)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('emits nothing page-URL related without the key (unchanged behaviour)', async () => {
|
|
194
|
+
const code = await runPlugin({})
|
|
195
|
+
|
|
196
|
+
expect(code).toContain('page: 1')
|
|
197
|
+
expect(code).not.toContain('pageUrlRef')
|
|
198
|
+
expect(code).not.toContain('router.query.page')
|
|
199
|
+
expect(code).not.toContain('useRouter')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('ignores the key on an infinite-scroll list, which has no pages to name', async () => {
|
|
203
|
+
const code = await runPlugin({ pageUrlParamKey: 'page', infiniteScroll: true })
|
|
204
|
+
|
|
205
|
+
expect(code).not.toContain('router.query.page')
|
|
206
|
+
expect(code).not.toContain('pageUrlRef')
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import {
|
|
3
|
+
PRODUCT_DISCOUNT_SORT_FIELD,
|
|
4
|
+
buildProductDiscountPercentageSql,
|
|
5
|
+
buildProductEffectivePriceSql,
|
|
6
|
+
generateSortFallbackFieldHelper,
|
|
7
|
+
generateSortFieldSqlHelper,
|
|
8
|
+
generateSortTiebreakSql,
|
|
9
|
+
supportsProductPriceSort,
|
|
10
|
+
} from '../src/product-price-sort'
|
|
11
|
+
import { generatePostgreSQLFetcher } from '../src/fetchers/postgresql'
|
|
12
|
+
import { generateTeleportFetcher } from '../src/fetchers/teleport'
|
|
13
|
+
import { generateDataSourceFetcher } from '../src/data-source-fetchers'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ordering a products list by what customers actually pay.
|
|
17
|
+
*
|
|
18
|
+
* The sort happens in SQL, before any row reaches JavaScript, and the list is
|
|
19
|
+
* paginated — so re-sorting the fetched page would only reorder 20 of N
|
|
20
|
+
* products. The database therefore has to compute the discounted price itself.
|
|
21
|
+
*
|
|
22
|
+
* These tests cannot execute the SQL (no Postgres here), so they pin the two
|
|
23
|
+
* things that are checkable: the emitted route still parses, and the expression
|
|
24
|
+
* says what the discount rule says.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const PG_CONFIG = { connectionString: 'postgres://user:pw@host:5432/db' }
|
|
28
|
+
|
|
29
|
+
describe('generateSortFieldSqlHelper', () => {
|
|
30
|
+
it('rewrites price only for the products table', () => {
|
|
31
|
+
expect(supportsProductPriceSort('teleport_products')).toBe(true)
|
|
32
|
+
expect(supportsProductPriceSort('teleport_orders')).toBe(false)
|
|
33
|
+
|
|
34
|
+
expect(generateSortFieldSqlHelper('teleport_products')).toContain('if (field === "price")')
|
|
35
|
+
// Every other table gets the identity, so a fetcher can embed it
|
|
36
|
+
// unconditionally and no unrelated sort can change.
|
|
37
|
+
const other = generateSortFieldSqlHelper('teleport_orders')
|
|
38
|
+
expect(other).not.toContain('if (field ===')
|
|
39
|
+
expect(other).toContain('return field')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('emits the SQL as a double-quoted literal, carrying no template syntax', () => {
|
|
43
|
+
// The helper is interpolated into a fetcher's template literal. A backtick
|
|
44
|
+
// or a `${` inside it would close or interpolate that template — which is
|
|
45
|
+
// exactly how this first went wrong.
|
|
46
|
+
const helper = generateSortFieldSqlHelper('teleport_products')
|
|
47
|
+
expect(helper).not.toContain('`')
|
|
48
|
+
expect(helper).not.toContain('${')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('is a syntactically valid JS function', () => {
|
|
52
|
+
expect(() =>
|
|
53
|
+
parse(generateSortFieldSqlHelper('teleport_products'), { sourceType: 'script' })
|
|
54
|
+
).not.toThrow()
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('the effective-price expression', () => {
|
|
59
|
+
const sql = buildProductEffectivePriceSql()
|
|
60
|
+
|
|
61
|
+
it('falls back to the list price when nothing is live', () => {
|
|
62
|
+
expect(sql.startsWith('COALESCE((')).toBe(true)
|
|
63
|
+
expect(sql.trimEnd().endsWith('), "price"::numeric)')).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('applies the same rule as the discount module', () => {
|
|
67
|
+
// Percentage off the list price, capped at 100; fixed subtracted; floored
|
|
68
|
+
// at zero and rounded to the cent — `computeDiscountedPrice`'s contract.
|
|
69
|
+
expect(sql).toContain(`"price"::numeric * (1 - LEAST(entry.amount, 100) / 100)`)
|
|
70
|
+
expect(sql).toContain(`"price"::numeric - entry.amount`)
|
|
71
|
+
expect(sql).toContain('ROUND(GREATEST(0,')
|
|
72
|
+
expect(sql).toContain(', 2)')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('takes the FIRST live entry in array order, like resolveActiveDiscount', () => {
|
|
76
|
+
expect(sql).toContain('WITH ORDINALITY')
|
|
77
|
+
expect(sql).toContain('ORDER BY entry.ord')
|
|
78
|
+
expect(sql).toContain('LIMIT 1')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('honours the half-open window', () => {
|
|
82
|
+
// `[startsAt, endsAt)` — inclusive start, exclusive end, so back-to-back
|
|
83
|
+
// discounts hand over with no overlap.
|
|
84
|
+
expect(sql).toContain('entry.starts_at <=')
|
|
85
|
+
expect(sql).toContain('entry.ends_at >')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('ignores entries the parser would drop', () => {
|
|
89
|
+
expect(sql).toContain('entry.amount > 0')
|
|
90
|
+
expect(sql).toContain(`entry.kind IN ('percentage', 'fixed')`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('never casts anything that could raise', () => {
|
|
94
|
+
// It runs inside ORDER BY for the whole table: one unparseable row would
|
|
95
|
+
// empty the products page.
|
|
96
|
+
// JSON is cast only when the text already looks like an array…
|
|
97
|
+
expect(sql).toContain(`btrim("discounts") NOT LIKE '[%'`)
|
|
98
|
+
expect(sql).toContain(`'[]'::jsonb`)
|
|
99
|
+
// …the value only when it is a bare decimal…
|
|
100
|
+
expect(sql).toContain(`~ '^[0-9]+(\\.[0-9]+)?$'`)
|
|
101
|
+
// …and the window bounds are compared as TEXT, never cast to a timestamp.
|
|
102
|
+
// `toISOString()` is fixed-width UTC, so it sorts lexicographically in the
|
|
103
|
+
// same order it sorts chronologically.
|
|
104
|
+
expect(sql).not.toContain('::timestamp')
|
|
105
|
+
expect(sql).toContain(`to_char(now() AT TIME ZONE 'UTC'`)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe('the generated Postgres route', () => {
|
|
110
|
+
const productsRoute = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products')
|
|
111
|
+
|
|
112
|
+
it('parses as valid JavaScript', () => {
|
|
113
|
+
expect(() => parse(productsRoute, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('orders through the mapper rather than the raw field', () => {
|
|
117
|
+
expect(productsRoute).toContain('sortFieldSql(sort.field)')
|
|
118
|
+
expect(productsRoute).toContain('if (field === "price")')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('keeps a plain-column ORDER BY to fall back to', () => {
|
|
122
|
+
// If a database ever rejects the sub-select, the page must degrade to the
|
|
123
|
+
// previous ordering rather than serve nothing.
|
|
124
|
+
expect(productsRoute).toContain('const usedDiscountAwareSort = orderBySql !== plainOrderBySql')
|
|
125
|
+
expect(productsRoute).toContain('if (!usedDiscountAwareSort) throw sortError')
|
|
126
|
+
expect(productsRoute).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('leaves every other table ordering exactly as it was', () => {
|
|
130
|
+
const ordersRoute = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_orders')
|
|
131
|
+
expect(ordersRoute).toContain('sortFieldSql(sort.field)')
|
|
132
|
+
expect(ordersRoute).not.toContain('if (field ===')
|
|
133
|
+
expect(ordersRoute).not.toContain('jsonb_array_elements')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('still parses for a non-products table', () => {
|
|
137
|
+
expect(() =>
|
|
138
|
+
parse(generatePostgreSQLFetcher(PG_CONFIG, 'teleport_orders'), { sourceType: 'module' })
|
|
139
|
+
).not.toThrow()
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe('the discount ordering', () => {
|
|
144
|
+
const sql = buildProductDiscountPercentageSql()
|
|
145
|
+
|
|
146
|
+
it('sorts on the real `discounts` column, never a virtual field name', () => {
|
|
147
|
+
// The same field reaches sixteen other fetchers and the editor's data API,
|
|
148
|
+
// none of which know about the rewrite. A name that is not a column becomes
|
|
149
|
+
// `ORDER BY discount` and fails the entire query; the real column can only
|
|
150
|
+
// ever be ordered by uselessly.
|
|
151
|
+
expect(PRODUCT_DISCOUNT_SORT_FIELD).toBe('discounts')
|
|
152
|
+
expect(generateSortFieldSqlHelper('teleport_products')).toContain('if (field === "discounts")')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('measures the markdown as a percentage of the list price', () => {
|
|
156
|
+
// Comparable across a mixed catalogue: $20 off a $2000 sofa is a worse deal
|
|
157
|
+
// than $5 off a $10 mug, and an absolute saving would rank them backwards.
|
|
158
|
+
expect(sql).toContain('* 100')
|
|
159
|
+
expect(sql).toContain('/ "price"::numeric')
|
|
160
|
+
expect(sql).toContain(buildProductEffectivePriceSql())
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('cannot divide by zero, and treats a priceless row as undiscounted', () => {
|
|
164
|
+
// `NULL > 0` is NULL, which is not true — so a NULL price takes the ELSE.
|
|
165
|
+
expect(sql).toContain('WHEN "price"::numeric > 0')
|
|
166
|
+
expect(sql).toContain('ELSE 0')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('rounds, so ties are stable rather than floating-point noise', () => {
|
|
170
|
+
expect(sql).toContain('ROUND(')
|
|
171
|
+
expect(sql).toContain(', 4)')
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('degrading safely', () => {
|
|
176
|
+
it('falls back to a column that is certain to exist', () => {
|
|
177
|
+
const helper = generateSortFallbackFieldHelper('teleport_products')
|
|
178
|
+
// Never back to `discounts` itself: it is the JSON column, and a store
|
|
179
|
+
// provisioned before discounts shipped does not have it at all — retrying
|
|
180
|
+
// with it would fail exactly like the expression did.
|
|
181
|
+
expect(helper).toContain('if (field === "price" || field === "discounts")')
|
|
182
|
+
expect(helper).toContain('return "price"')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('leaves other tables and other fields alone', () => {
|
|
186
|
+
const helper = generateSortFallbackFieldHelper('teleport_orders')
|
|
187
|
+
expect(helper).not.toContain('if (field ===')
|
|
188
|
+
expect(helper).toContain('return field')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('is valid JS for both tables', () => {
|
|
192
|
+
expect(() =>
|
|
193
|
+
parse(generateSortFallbackFieldHelper('teleport_products'), { sourceType: 'script' })
|
|
194
|
+
).not.toThrow()
|
|
195
|
+
expect(() =>
|
|
196
|
+
parse(generateSortFallbackFieldHelper('teleport_orders'), { sourceType: 'script' })
|
|
197
|
+
).not.toThrow()
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('stable pagination', () => {
|
|
202
|
+
it('breaks ties on the products table so pages cannot repeat or skip rows', () => {
|
|
203
|
+
// "Sort by discount" ties every undiscounted product at zero. Equal rows
|
|
204
|
+
// with no defined order can appear on two pages, or on none.
|
|
205
|
+
expect(generateSortTiebreakSql('teleport_products')).toBe(', "id" ASC')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('adds nothing to a table it does not own', () => {
|
|
209
|
+
expect(generateSortTiebreakSql('teleport_orders')).toBe('')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('is applied to both the real and the fallback ORDER BY', () => {
|
|
213
|
+
const route = generatePostgreSQLFetcher(PG_CONFIG, 'teleport_products')
|
|
214
|
+
const orderByLines = route
|
|
215
|
+
.split('\n')
|
|
216
|
+
.filter((line) => line.includes('ORDER BY') && line.includes('join'))
|
|
217
|
+
expect(orderByLines).toHaveLength(2)
|
|
218
|
+
orderByLines.forEach((line) => expect(line).toContain('"id" ASC'))
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The half of this that was missed the first time.
|
|
224
|
+
*
|
|
225
|
+
* A generated storefront's products table is TELEPORT-hosted, so its route is
|
|
226
|
+
* built by `generateTeleportFetcher` — not by `generatePostgreSQLFetcher`,
|
|
227
|
+
* which every test above exercises. The rewrite went into the Postgres fetcher
|
|
228
|
+
* alone, so a real store still ordered by the undiscounted list price while the
|
|
229
|
+
* whole suite passed.
|
|
230
|
+
*
|
|
231
|
+
* These tests go through `generateFetcher`, the function the project generator
|
|
232
|
+
* actually calls, so what is asserted is the route a store really ships.
|
|
233
|
+
*/
|
|
234
|
+
describe('the fetcher a generated storefront actually gets', () => {
|
|
235
|
+
const teleportProducts = generateDataSourceFetcher(
|
|
236
|
+
{ type: 'teleport', config: { host: 'db', database: 'store' } },
|
|
237
|
+
'teleport_products'
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
it('is discount-aware for a teleport-hosted products table', () => {
|
|
241
|
+
expect(teleportProducts).toContain('sortFieldSql(sort.field)')
|
|
242
|
+
expect(teleportProducts).toContain('if (field === "price")')
|
|
243
|
+
expect(teleportProducts).toContain('if (field === "discounts")')
|
|
244
|
+
expect(teleportProducts).toContain('jsonb_array_elements')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('keeps the plain-column fallback and the tiebreaker', () => {
|
|
248
|
+
expect(teleportProducts).toContain(
|
|
249
|
+
'const usedDiscountAwareSort = orderBySql !== plainOrderBySql'
|
|
250
|
+
)
|
|
251
|
+
expect(teleportProducts).toContain('if (!usedDiscountAwareSort) throw sortError')
|
|
252
|
+
expect(teleportProducts).toContain('baseSql + plainOrderBySql + sqlTail')
|
|
253
|
+
expect(teleportProducts).toContain('"id" ASC')
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('parses as valid JavaScript', () => {
|
|
257
|
+
expect(() => parse(teleportProducts, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('leaves a teleport-hosted table that is not products alone', () => {
|
|
261
|
+
const orders = generateDataSourceFetcher(
|
|
262
|
+
{ type: 'teleport', config: { host: 'db', database: 'store' } },
|
|
263
|
+
'teleport_orders'
|
|
264
|
+
)
|
|
265
|
+
expect(orders).not.toContain('if (field ===')
|
|
266
|
+
expect(orders).not.toContain('jsonb_array_elements')
|
|
267
|
+
expect(() => parse(orders, { sourceType: 'module', plugins: ['jsx'] })).not.toThrow()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('is discount-aware for a self-hosted Postgres products table too', () => {
|
|
271
|
+
const pg = generateDataSourceFetcher(
|
|
272
|
+
{ type: 'postgresql', config: PG_CONFIG },
|
|
273
|
+
'teleport_products'
|
|
274
|
+
)
|
|
275
|
+
expect(pg).toContain('if (field === "price")')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The guard that makes the miss above impossible to repeat: every SQL data
|
|
280
|
+
* source that can serve a products table must route its sorts through
|
|
281
|
+
* `sortFieldSql`. A new Postgres-compatible fetcher, or a rewrite of an
|
|
282
|
+
* existing one, fails here rather than silently shipping the list price.
|
|
283
|
+
*/
|
|
284
|
+
it.each([
|
|
285
|
+
['teleport', { host: 'db', database: 'store' }],
|
|
286
|
+
['postgresql', PG_CONFIG],
|
|
287
|
+
['cockroachdb', PG_CONFIG],
|
|
288
|
+
])('routes %s sorts through the mapper', (type, config) => {
|
|
289
|
+
const route = generateDataSourceFetcher(
|
|
290
|
+
{ type, config } as unknown as Parameters<typeof generateDataSourceFetcher>[0],
|
|
291
|
+
'teleport_products'
|
|
292
|
+
)
|
|
293
|
+
expect(route).toContain('sortFieldSql(sort.field)')
|
|
294
|
+
expect(route).not.toMatch(/return `\$\{sort\.field\} \$\{order\}`/)
|
|
295
|
+
})
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
describe('the teleport fetcher, directly', () => {
|
|
299
|
+
it('emits the helpers exactly once, so the route has no duplicate declaration', () => {
|
|
300
|
+
const route = generateTeleportFetcher({ host: 'db' }, 'teleport_products')
|
|
301
|
+
expect(route.split('function sortFieldSql(')).toHaveLength(2)
|
|
302
|
+
expect(route.split('function sortFallbackField(')).toHaveLength(2)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('does not add an ORDER BY to the count query', () => {
|
|
306
|
+
// The count route shares this file; a tiebreaker or a sub-select there
|
|
307
|
+
// would be wasted work at best and a syntax error at worst.
|
|
308
|
+
const route = generateTeleportFetcher({ host: 'db' }, 'teleport_products')
|
|
309
|
+
const countBody = route.slice(route.indexOf('async function getCount'))
|
|
310
|
+
expect(countBody).not.toContain('ORDER BY ${')
|
|
311
|
+
expect(countBody).not.toContain('sortFieldSql(')
|
|
312
|
+
})
|
|
313
|
+
})
|
|
@@ -96,9 +96,12 @@ describe('pagination plugin — search input URL two-way sync (searchUrlParamKey
|
|
|
96
96
|
// useRouter is injected for the effects.
|
|
97
97
|
expect(code).toContain('const router = useRouter()')
|
|
98
98
|
|
|
99
|
-
// Write-back (debounced query → URL), keyed on the DEBOUNCED value
|
|
100
|
-
|
|
101
|
-
expect(code).toContain('
|
|
99
|
+
// Write-back (debounced query → URL), keyed on the DEBOUNCED value and
|
|
100
|
+
// routed through the shared writer so it cannot race the page's own write.
|
|
101
|
+
expect(code).toContain('const __tqQuerySyncRef = useRef(')
|
|
102
|
+
expect(code).toContain(
|
|
103
|
+
'__tqWriteQueryParam("searchKeyword", ds_0_state.debouncedQuery === "" || ds_0_state.debouncedQuery == null ? undefined : ds_0_state.debouncedQuery)'
|
|
104
|
+
)
|
|
102
105
|
expect(code).toContain('}, [ds_0_state.debouncedQuery, router.isReady])')
|
|
103
106
|
|
|
104
107
|
// Read-back (URL → input), functional setState bail-out (loop-free).
|
|
@@ -115,6 +118,7 @@ describe('pagination plugin — search input URL two-way sync (searchUrlParamKey
|
|
|
115
118
|
expect(code).not.toContain('searchKeyword')
|
|
116
119
|
expect(code).not.toContain('window.location.search')
|
|
117
120
|
expect(code).not.toContain('router.replace')
|
|
121
|
+
expect(code).not.toContain('__tqWriteQueryParam')
|
|
118
122
|
expect(code).not.toContain('useRouter')
|
|
119
123
|
})
|
|
120
124
|
})
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { UIDLElementNode, UIDLNode } from '@teleporthq/teleport-types';
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated Not part of the generation pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Array-mapper pagination is generated by `pagination-plugin.ts` (with
|
|
6
|
+
* `numbered-pagination.ts`, `infinite-scroll.ts` and `pagination-controls.ts`).
|
|
7
|
+
* This module models an older client-side slicing design and is kept only
|
|
8
|
+
* because it is re-exported from the package index; nothing in the pipeline
|
|
9
|
+
* calls it. Add nothing here — the two would drift apart in silence.
|
|
10
|
+
*/
|
|
2
11
|
export interface ArrayMapperPaginationInfo {
|
|
3
12
|
paginationNodeId: string;
|
|
4
13
|
dataSourceIdentifier: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"array-mapper-pagination.d.ts","sourceRoot":"","sources":["../../src/array-mapper-pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAGtE,MAAM,WAAW,yBAAyB;IACxC,gBAAgB,EAAE,MAAM,CAAA;IACxB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IACnC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,eAAe,CAAA;IAC/B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,eAAe,GAAG,IAAI,CAAA;IAClC,UAAU,EAAE,eAAe,GAAG,IAAI,CAAA;CACnC;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,kBAAkB,EAAE,CA2CxE;AAED,wBAAgB,uBAAuB,CACrC,gBAAgB,EAAE,MAAM,EACxB,oBAAoB,EAAE,MAAM,EAC5B,OAAO,GAAE,MAAW,EACpB,YAAY,CAAC,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,EACjE,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,yBAAyB,CA+B3B"}
|
|
1
|
+
{"version":3,"file":"array-mapper-pagination.d.ts","sourceRoot":"","sources":["../../src/array-mapper-pagination.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAGtE;;;;;;;;GAQG;AAEH,MAAM,WAAW,yBAAyB;IACxC,gBAAgB,EAAE,MAAM,CAAA;IACxB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,cAAc,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,gBAAgB,EAAE,MAAM,CAAA;IACxB,aAAa,EAAE,MAAM,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,0BAA0B,CAAC,EAAE,MAAM,CAAA;IACnC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,EAAE,eAAe,CAAA;IAC/B,oBAAoB,EAAE,MAAM,CAAA;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,eAAe,GAAG,IAAI,CAAA;IAClC,UAAU,EAAE,eAAe,GAAG,IAAI,CAAA;CACnC;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,kBAAkB,EAAE,CA2CxE;AAED,wBAAgB,uBAAuB,CACrC,gBAAgB,EAAE,MAAM,EACxB,oBAAoB,EAAE,MAAM,EAC5B,OAAO,GAAE,MAAW,EACpB,YAAY,CAAC,EAAE;IAAE,aAAa,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAAE,EACjE,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,yBAAyB,CA+B3B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"array-mapper-pagination.js","sourceRoot":"","sources":["../../src/array-mapper-pagination.ts"],"names":[],"mappings":";;;AACA,+DAAuD;
|
|
1
|
+
{"version":3,"file":"array-mapper-pagination.js","sourceRoot":"","sources":["../../src/array-mapper-pagination.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as types from '@babel/types';
|
|
2
|
+
/**
|
|
3
|
+
* The client-side "how many rows match?" fetch that drives `ds_N_maxPages`.
|
|
4
|
+
*
|
|
5
|
+
* One builder for every caller, because the two hand-written copies had already
|
|
6
|
+
* drifted: the search-enabled list sent the query, the columns and the filters,
|
|
7
|
+
* while the paginated-only list fetched a bare `/api/<file>-count` with NO
|
|
8
|
+
* params at all — so a filtered list counted the WHOLE table and "Next" stayed
|
|
9
|
+
* enabled long past the end of the filtered results.
|
|
10
|
+
*
|
|
11
|
+
* The endpoint deliberately receives no `sorts` and no `limit`: ordering cannot
|
|
12
|
+
* change how many rows match, and a limit would count the page rather than the
|
|
13
|
+
* result set.
|
|
14
|
+
*/
|
|
15
|
+
interface CountEffectOptions {
|
|
16
|
+
/** `<fileName>-count` is the generated API route. */
|
|
17
|
+
fileName: string;
|
|
18
|
+
/** Divisor for the page count; the mapper's resolved `perPage`. */
|
|
19
|
+
perPage: number;
|
|
20
|
+
/** Name of the `setDs_N_maxPages` setter. */
|
|
21
|
+
setMaxPagesVar: string;
|
|
22
|
+
/**
|
|
23
|
+
* Params sent to the count endpoint: the search query (when the list has
|
|
24
|
+
* one), `queryColumns`, and the filter array. Built by the caller because
|
|
25
|
+
* only it knows how this usage's filter destinations resolve to expressions.
|
|
26
|
+
*/
|
|
27
|
+
urlParams: types.ObjectProperty[];
|
|
28
|
+
/** Dependencies that should re-run the count. */
|
|
29
|
+
deps: types.Expression[];
|
|
30
|
+
}
|
|
31
|
+
export declare const buildCountFetchStatement: (options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'urlParams'>) => types.Statement;
|
|
32
|
+
/**
|
|
33
|
+
* The whole `useEffect(...)` wrapper.
|
|
34
|
+
*
|
|
35
|
+
* ⛔ No skip-on-mount guard, on purpose: a page seeds `maxPages` from the
|
|
36
|
+
* build-time `getStaticProps` count, and that snapshot goes stale the moment
|
|
37
|
+
* rows are added after the build — leaving "Next" disabled on a list that has
|
|
38
|
+
* grown. Re-counting on mount is what corrects it.
|
|
39
|
+
*/
|
|
40
|
+
export declare const buildCountFetchEffect: (options: CountEffectOptions) => types.ExpressionStatement;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=count-effect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"count-effect.d.ts","sourceRoot":"","sources":["../../src/count-effect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;GAYG;AAEH,UAAU,kBAAkB;IAC1B,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAA;IAChB,mEAAmE;IACnE,OAAO,EAAE,MAAM,CAAA;IACf,6CAA6C;IAC7C,cAAc,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,SAAS,EAAE,KAAK,CAAC,cAAc,EAAE,CAAA;IACjC,iDAAiD;IACjD,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;CACzB;AA6BD,eAAO,MAAM,wBAAwB,YAC1B,KAAK,kBAAkB,EAAE,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,CAAC,KACzF,MAAM,SA0DN,CAAA;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,YAAa,kBAAkB,KAAG,MAAM,mBAMvE,CAAA"}
|