@teleporthq/teleport-plugin-next-data-source 0.43.59 → 0.43.61
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-discounts.test.ts +196 -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/cache/ast.d.ts.map +1 -1
- package/dist/cjs/cache/ast.js +2 -2
- package/dist/cjs/cache/ast.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 +508 -164
- 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.map +1 -1
- package/dist/cjs/transformations/ecommerce-product.js +1 -1
- package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils.d.ts.map +1 -1
- package/dist/cjs/utils.js +5 -7
- package/dist/cjs/utils.js.map +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/cache/ast.d.ts.map +1 -1
- package/dist/esm/cache/ast.js +2 -2
- package/dist/esm/cache/ast.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 +510 -166
- 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.map +1 -1
- package/dist/esm/transformations/ecommerce-product.js +2 -2
- package/dist/esm/transformations/ecommerce-product.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils.d.ts.map +1 -1
- package/dist/esm/utils.js +6 -8
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/array-mapper-pagination.ts +10 -0
- package/src/cache/ast.ts +2 -2
- 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 +758 -351
- package/src/product-price-sort.ts +204 -0
- package/src/transformations/ecommerce-product.ts +33 -7
- package/src/utils.ts +6 -8
- 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,137 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The client-side "how many rows match?" fetch that drives `ds_N_maxPages`.
|
|
5
|
+
*
|
|
6
|
+
* One builder for every caller, because the two hand-written copies had already
|
|
7
|
+
* drifted: the search-enabled list sent the query, the columns and the filters,
|
|
8
|
+
* while the paginated-only list fetched a bare `/api/<file>-count` with NO
|
|
9
|
+
* params at all — so a filtered list counted the WHOLE table and "Next" stayed
|
|
10
|
+
* enabled long past the end of the filtered results.
|
|
11
|
+
*
|
|
12
|
+
* The endpoint deliberately receives no `sorts` and no `limit`: ordering cannot
|
|
13
|
+
* change how many rows match, and a limit would count the page rather than the
|
|
14
|
+
* result set.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface CountEffectOptions {
|
|
18
|
+
/** `<fileName>-count` is the generated API route. */
|
|
19
|
+
fileName: string
|
|
20
|
+
/** Divisor for the page count; the mapper's resolved `perPage`. */
|
|
21
|
+
perPage: number
|
|
22
|
+
/** Name of the `setDs_N_maxPages` setter. */
|
|
23
|
+
setMaxPagesVar: string
|
|
24
|
+
/**
|
|
25
|
+
* Params sent to the count endpoint: the search query (when the list has
|
|
26
|
+
* one), `queryColumns`, and the filter array. Built by the caller because
|
|
27
|
+
* only it knows how this usage's filter destinations resolve to expressions.
|
|
28
|
+
*/
|
|
29
|
+
urlParams: types.ObjectProperty[]
|
|
30
|
+
/** Dependencies that should re-run the count. */
|
|
31
|
+
deps: types.Expression[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* `data.count === 0 ? 0 : Math.ceil(data.count / perPage)`
|
|
36
|
+
*
|
|
37
|
+
* Zero is special-cased rather than left to `Math.ceil(0 / n)` so the intent —
|
|
38
|
+
* "no results at all", which numbered pagination renders as an empty strip — is
|
|
39
|
+
* visible in the generated code.
|
|
40
|
+
*/
|
|
41
|
+
const buildMaxPagesExpression = (perPage: number): types.Expression =>
|
|
42
|
+
types.conditionalExpression(
|
|
43
|
+
types.binaryExpression(
|
|
44
|
+
'===',
|
|
45
|
+
types.memberExpression(types.identifier('data'), types.identifier('count')),
|
|
46
|
+
types.numericLiteral(0)
|
|
47
|
+
),
|
|
48
|
+
types.numericLiteral(0),
|
|
49
|
+
types.callExpression(
|
|
50
|
+
types.memberExpression(types.identifier('Math'), types.identifier('ceil')),
|
|
51
|
+
[
|
|
52
|
+
types.binaryExpression(
|
|
53
|
+
'/',
|
|
54
|
+
types.memberExpression(types.identifier('data'), types.identifier('count')),
|
|
55
|
+
types.numericLiteral(perPage)
|
|
56
|
+
),
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
export const buildCountFetchStatement = (
|
|
62
|
+
options: Pick<CountEffectOptions, 'fileName' | 'perPage' | 'setMaxPagesVar' | 'urlParams'>
|
|
63
|
+
): types.Statement =>
|
|
64
|
+
types.expressionStatement(
|
|
65
|
+
types.callExpression(
|
|
66
|
+
types.memberExpression(
|
|
67
|
+
types.callExpression(
|
|
68
|
+
types.memberExpression(
|
|
69
|
+
types.callExpression(types.identifier('fetch'), [
|
|
70
|
+
types.templateLiteral(
|
|
71
|
+
[
|
|
72
|
+
types.templateElement({
|
|
73
|
+
raw: `/api/${options.fileName}-count?`,
|
|
74
|
+
cooked: `/api/${options.fileName}-count?`,
|
|
75
|
+
}),
|
|
76
|
+
types.templateElement({ raw: '', cooked: '' }),
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
types.newExpression(types.identifier('URLSearchParams'), [
|
|
80
|
+
types.objectExpression(options.urlParams),
|
|
81
|
+
]),
|
|
82
|
+
]
|
|
83
|
+
),
|
|
84
|
+
]),
|
|
85
|
+
types.identifier('then')
|
|
86
|
+
),
|
|
87
|
+
[
|
|
88
|
+
types.arrowFunctionExpression(
|
|
89
|
+
[types.identifier('res')],
|
|
90
|
+
types.callExpression(
|
|
91
|
+
types.memberExpression(types.identifier('res'), types.identifier('json')),
|
|
92
|
+
[]
|
|
93
|
+
)
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
),
|
|
97
|
+
types.identifier('then')
|
|
98
|
+
),
|
|
99
|
+
[
|
|
100
|
+
types.arrowFunctionExpression(
|
|
101
|
+
[types.identifier('data')],
|
|
102
|
+
types.blockStatement([
|
|
103
|
+
types.ifStatement(
|
|
104
|
+
types.logicalExpression(
|
|
105
|
+
'&&',
|
|
106
|
+
types.identifier('data'),
|
|
107
|
+
types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))
|
|
108
|
+
),
|
|
109
|
+
types.blockStatement([
|
|
110
|
+
types.expressionStatement(
|
|
111
|
+
types.callExpression(types.identifier(options.setMaxPagesVar), [
|
|
112
|
+
buildMaxPagesExpression(options.perPage),
|
|
113
|
+
])
|
|
114
|
+
),
|
|
115
|
+
])
|
|
116
|
+
),
|
|
117
|
+
])
|
|
118
|
+
),
|
|
119
|
+
]
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The whole `useEffect(...)` wrapper.
|
|
125
|
+
*
|
|
126
|
+
* ⛔ No skip-on-mount guard, on purpose: a page seeds `maxPages` from the
|
|
127
|
+
* build-time `getStaticProps` count, and that snapshot goes stale the moment
|
|
128
|
+
* rows are added after the build — leaving "Next" disabled on a list that has
|
|
129
|
+
* grown. Re-counting on mount is what corrects it.
|
|
130
|
+
*/
|
|
131
|
+
export const buildCountFetchEffect = (options: CountEffectOptions): types.ExpressionStatement =>
|
|
132
|
+
types.expressionStatement(
|
|
133
|
+
types.callExpression(types.identifier('useEffect'), [
|
|
134
|
+
types.arrowFunctionExpression([], types.blockStatement([buildCountFetchStatement(options)])),
|
|
135
|
+
types.arrayExpression(options.deps),
|
|
136
|
+
])
|
|
137
|
+
)
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
generateSafeJSONParseCode,
|
|
5
5
|
generateSearchEscapeHelpersCode,
|
|
6
6
|
} from '../utils'
|
|
7
|
+
import {
|
|
8
|
+
generateSortFallbackFieldHelper,
|
|
9
|
+
generateSortFieldSqlHelper,
|
|
10
|
+
generateSortTiebreakSql,
|
|
11
|
+
} from '../product-price-sort'
|
|
7
12
|
|
|
8
13
|
interface PostgreSQLConfig {
|
|
9
14
|
connectionString?: string
|
|
@@ -134,6 +139,10 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
134
139
|
|
|
135
140
|
${generateDateFormatterCode()}
|
|
136
141
|
|
|
142
|
+
${generateSortFieldSqlHelper(tableName)}
|
|
143
|
+
|
|
144
|
+
${generateSortFallbackFieldHelper(tableName)}
|
|
145
|
+
|
|
137
146
|
export default async function handler(req, res) {
|
|
138
147
|
const client = getClient()
|
|
139
148
|
|
|
@@ -198,36 +207,66 @@ export default async function handler(req, res) {
|
|
|
198
207
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
199
208
|
}
|
|
200
209
|
|
|
201
|
-
// Handle sorts - new array format
|
|
210
|
+
// Handle sorts - new array format. Two ORDER BY clauses are built: the one
|
|
211
|
+
// actually used, and a plain-column twin kept only as the fallback below.
|
|
212
|
+
let orderBySql = ''
|
|
213
|
+
let plainOrderBySql = ''
|
|
202
214
|
if (sorts) {
|
|
203
215
|
const parsedSorts = safeJSONParse(sorts)
|
|
204
216
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}).filter(Boolean)
|
|
217
|
+
const valid = parsedSorts.filter((sort) => sort && sort.field)
|
|
218
|
+
const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
219
|
+
const orderClauses = valid.map((sort) => \`\${sortFieldSql(sort.field)} \${orderOf(sort)}\`)
|
|
220
|
+
const plainClauses = valid.map((sort) => \`\${sortFallbackField(sort.field)} \${orderOf(sort)}\`)
|
|
210
221
|
|
|
211
222
|
if (orderClauses.length > 0) {
|
|
212
|
-
|
|
223
|
+
// A deterministic tiebreaker keeps pagination stable: "sort by
|
|
224
|
+
// discount" ties every undiscounted product at zero, and equal rows
|
|
225
|
+
// with no defined order can repeat across pages or vanish between them.
|
|
226
|
+
orderBySql = \` ORDER BY \${orderClauses.join(', ')}${generateSortTiebreakSql(
|
|
227
|
+
tableName
|
|
228
|
+
)}\`
|
|
229
|
+
plainOrderBySql = \` ORDER BY \${plainClauses.join(', ')}${generateSortTiebreakSql(
|
|
230
|
+
tableName
|
|
231
|
+
)}\`
|
|
213
232
|
}
|
|
214
233
|
}
|
|
215
234
|
} else if (sortBy) {
|
|
216
|
-
|
|
235
|
+
orderBySql = \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
|
|
236
|
+
plainOrderBySql = orderBySql
|
|
217
237
|
}
|
|
218
|
-
|
|
238
|
+
const usedDiscountAwareSort = orderBySql !== plainOrderBySql
|
|
239
|
+
|
|
219
240
|
const limitValue = limit || perPage
|
|
220
241
|
const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
|
|
221
|
-
|
|
242
|
+
|
|
243
|
+
let sqlTail = ''
|
|
222
244
|
if (limitValue) {
|
|
223
|
-
|
|
245
|
+
sqlTail += \` LIMIT \${limitValue}\`
|
|
224
246
|
}
|
|
225
247
|
|
|
226
248
|
if (offsetValue !== undefined) {
|
|
227
|
-
|
|
249
|
+
sqlTail += \` OFFSET \${offsetValue}\`
|
|
228
250
|
}
|
|
251
|
+
|
|
252
|
+
const baseSql = sql
|
|
253
|
+
sql = baseSql + orderBySql + sqlTail
|
|
229
254
|
|
|
230
|
-
|
|
255
|
+
// The discount-aware price ordering is an inline sub-select over a JSON
|
|
256
|
+
// column. It is written to be unraisable, but it runs inside ORDER BY for
|
|
257
|
+
// the whole table — so if a database ever rejects it, fall back to ordering
|
|
258
|
+
// by the stored list price rather than serving an empty products page.
|
|
259
|
+
let result
|
|
260
|
+
try {
|
|
261
|
+
result = await client.query(sql, queryParams)
|
|
262
|
+
} catch (sortError) {
|
|
263
|
+
if (!usedDiscountAwareSort) throw sortError
|
|
264
|
+
console.warn(
|
|
265
|
+
'Discount-aware price sort failed; falling back to the list price:',
|
|
266
|
+
sortError && sortError.message
|
|
267
|
+
)
|
|
268
|
+
result = await client.query(baseSql + plainOrderBySql + sqlTail, queryParams)
|
|
269
|
+
}
|
|
231
270
|
const rows = Array.isArray(result?.rows) ? result.rows : []
|
|
232
271
|
const plainRows = rows.map((row) =>
|
|
233
272
|
row && typeof row.toJSON === 'function' ? row.toJSON() : row
|
package/src/fetchers/teleport.ts
CHANGED
|
@@ -10,6 +10,11 @@ import {
|
|
|
10
10
|
getTransformWrapperCode,
|
|
11
11
|
type EcommerceProductTransformOptions,
|
|
12
12
|
} from '../transformations'
|
|
13
|
+
import {
|
|
14
|
+
generateSortFallbackFieldHelper,
|
|
15
|
+
generateSortFieldSqlHelper,
|
|
16
|
+
generateSortTiebreakSql,
|
|
17
|
+
} from '../product-price-sort'
|
|
13
18
|
|
|
14
19
|
interface TeleportDBConfig {
|
|
15
20
|
host?: string
|
|
@@ -221,6 +226,10 @@ const processFilters = (filters, conditions, queryParams, paramIndex) => {
|
|
|
221
226
|
|
|
222
227
|
${generateDateFormatterCode()}
|
|
223
228
|
|
|
229
|
+
${generateSortFieldSqlHelper(tableName)}
|
|
230
|
+
|
|
231
|
+
${generateSortFallbackFieldHelper(tableName)}
|
|
232
|
+
|
|
224
233
|
// Matches DDL / dangerous statements the raw-query branch should refuse.
|
|
225
234
|
// Keep this list conservative — anything destructive or schema-changing is
|
|
226
235
|
// out of scope for a client-triggered fetch. SELECT and CTEs (WITH ...) are
|
|
@@ -346,35 +355,66 @@ export default async function handler(req, res) {
|
|
|
346
355
|
sql += \` WHERE \${conditions.join(' AND ')}\`
|
|
347
356
|
}
|
|
348
357
|
|
|
358
|
+
// Handle sorts - new array format. Two ORDER BY clauses are built: the one
|
|
359
|
+
// actually used, and a plain-column twin kept only as the fallback below.
|
|
360
|
+
let orderBySql = ''
|
|
361
|
+
let plainOrderBySql = ''
|
|
349
362
|
if (sorts) {
|
|
350
363
|
const parsedSorts = safeJSONParse(sorts)
|
|
351
364
|
if (Array.isArray(parsedSorts) && parsedSorts.length > 0) {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}).filter(Boolean)
|
|
365
|
+
const valid = parsedSorts.filter((sort) => sort && sort.field)
|
|
366
|
+
const orderOf = (sort) => (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
|
|
367
|
+
const orderClauses = valid.map((sort) => \`\${sortFieldSql(sort.field)} \${orderOf(sort)}\`)
|
|
368
|
+
const plainClauses = valid.map((sort) => \`\${sortFallbackField(sort.field)} \${orderOf(sort)}\`)
|
|
357
369
|
|
|
358
370
|
if (orderClauses.length > 0) {
|
|
359
|
-
|
|
371
|
+
// A deterministic tiebreaker keeps pagination stable: "sort by
|
|
372
|
+
// discount" ties every undiscounted product at zero, and equal rows
|
|
373
|
+
// with no defined order can repeat across pages or vanish between them.
|
|
374
|
+
orderBySql = \` ORDER BY \${orderClauses.join(', ')}${generateSortTiebreakSql(
|
|
375
|
+
tableName
|
|
376
|
+
)}\`
|
|
377
|
+
plainOrderBySql = \` ORDER BY \${plainClauses.join(', ')}${generateSortTiebreakSql(
|
|
378
|
+
tableName
|
|
379
|
+
)}\`
|
|
360
380
|
}
|
|
361
381
|
}
|
|
362
382
|
} else if (sortBy) {
|
|
363
|
-
|
|
383
|
+
orderBySql = \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
|
|
384
|
+
plainOrderBySql = orderBySql
|
|
364
385
|
}
|
|
365
|
-
|
|
386
|
+
const usedDiscountAwareSort = orderBySql !== plainOrderBySql
|
|
387
|
+
|
|
366
388
|
const limitValue = limit || perPage
|
|
367
389
|
const offsetValue = offset !== undefined ? parseInt(offset) : (page && perPage ? (parseInt(page) - 1) * parseInt(perPage) : undefined)
|
|
368
|
-
|
|
390
|
+
|
|
391
|
+
let sqlTail = ''
|
|
369
392
|
if (limitValue) {
|
|
370
|
-
|
|
393
|
+
sqlTail += \` LIMIT \${limitValue}\`
|
|
371
394
|
}
|
|
372
395
|
|
|
373
396
|
if (offsetValue !== undefined) {
|
|
374
|
-
|
|
397
|
+
sqlTail += \` OFFSET \${offsetValue}\`
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const baseSql = sql
|
|
401
|
+
sql = baseSql + orderBySql + sqlTail
|
|
402
|
+
|
|
403
|
+
// The discount-aware price ordering is an inline sub-select over a JSON
|
|
404
|
+
// column. It is written to be unraisable, but it runs inside ORDER BY for
|
|
405
|
+
// the whole table - so if a database ever rejects it, fall back to ordering
|
|
406
|
+
// by the stored list price rather than serving an empty products page.
|
|
407
|
+
let result
|
|
408
|
+
try {
|
|
409
|
+
result = await client.query(sql, queryParams)
|
|
410
|
+
} catch (sortError) {
|
|
411
|
+
if (!usedDiscountAwareSort) throw sortError
|
|
412
|
+
console.warn(
|
|
413
|
+
'Discount-aware price sort failed; falling back to the list price:',
|
|
414
|
+
sortError && sortError.message
|
|
415
|
+
)
|
|
416
|
+
result = await client.query(baseSql + plainOrderBySql + sqlTail, queryParams)
|
|
375
417
|
}
|
|
376
|
-
|
|
377
|
-
const result = await client.query(sql, queryParams)
|
|
378
418
|
const rows = Array.isArray(result?.rows) ? result.rows : []
|
|
379
419
|
const plainRows = rows.map((row) =>
|
|
380
420
|
row && typeof row.toJSON === 'function' ? row.toJSON() : row
|