@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.
Files changed (141) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +230 -0
  2. package/__tests__/ecommerce-product-discounts.test.ts +196 -0
  3. package/__tests__/infinite-scroll.test.ts +143 -0
  4. package/__tests__/numbered-pagination.test.ts +113 -0
  5. package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
  6. package/__tests__/page-url-sync.test.ts +208 -0
  7. package/__tests__/product-price-sort.ts +313 -0
  8. package/__tests__/search-url-sync.test.ts +7 -3
  9. package/dist/cjs/array-mapper-pagination.d.ts +9 -0
  10. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
  11. package/dist/cjs/array-mapper-pagination.js.map +1 -1
  12. package/dist/cjs/cache/ast.d.ts.map +1 -1
  13. package/dist/cjs/cache/ast.js +2 -2
  14. package/dist/cjs/cache/ast.js.map +1 -1
  15. package/dist/cjs/count-effect.d.ts +42 -0
  16. package/dist/cjs/count-effect.d.ts.map +1 -0
  17. package/dist/cjs/count-effect.js +81 -0
  18. package/dist/cjs/count-effect.js.map +1 -0
  19. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/postgresql.js +2 -1
  21. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  22. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/teleport.js +3 -2
  24. package/dist/cjs/fetchers/teleport.js.map +1 -1
  25. package/dist/cjs/infinite-scroll.d.ts +65 -0
  26. package/dist/cjs/infinite-scroll.d.ts.map +1 -0
  27. package/dist/cjs/infinite-scroll.js +190 -0
  28. package/dist/cjs/infinite-scroll.js.map +1 -0
  29. package/dist/cjs/numbered-pagination.d.ts +42 -0
  30. package/dist/cjs/numbered-pagination.d.ts.map +1 -0
  31. package/dist/cjs/numbered-pagination.js +167 -0
  32. package/dist/cjs/numbered-pagination.js.map +1 -0
  33. package/dist/cjs/page-reset.d.ts +42 -0
  34. package/dist/cjs/page-reset.d.ts.map +1 -0
  35. package/dist/cjs/page-reset.js +116 -0
  36. package/dist/cjs/page-reset.js.map +1 -0
  37. package/dist/cjs/page-url-sync.d.ts +42 -0
  38. package/dist/cjs/page-url-sync.d.ts.map +1 -0
  39. package/dist/cjs/page-url-sync.js +180 -0
  40. package/dist/cjs/page-url-sync.js.map +1 -0
  41. package/dist/cjs/pagination-controls.d.ts +55 -0
  42. package/dist/cjs/pagination-controls.d.ts.map +1 -0
  43. package/dist/cjs/pagination-controls.js +159 -0
  44. package/dist/cjs/pagination-controls.js.map +1 -0
  45. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  46. package/dist/cjs/pagination-plugin.js +508 -164
  47. package/dist/cjs/pagination-plugin.js.map +1 -1
  48. package/dist/cjs/product-price-sort.d.ts +116 -0
  49. package/dist/cjs/product-price-sort.d.ts.map +1 -0
  50. package/dist/cjs/product-price-sort.js +169 -0
  51. package/dist/cjs/product-price-sort.js.map +1 -0
  52. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  53. package/dist/cjs/transformations/ecommerce-product.js +1 -1
  54. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  55. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  56. package/dist/cjs/utils.d.ts.map +1 -1
  57. package/dist/cjs/utils.js +5 -7
  58. package/dist/cjs/utils.js.map +1 -1
  59. package/dist/esm/array-mapper-pagination.d.ts +9 -0
  60. package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
  61. package/dist/esm/array-mapper-pagination.js.map +1 -1
  62. package/dist/esm/cache/ast.d.ts.map +1 -1
  63. package/dist/esm/cache/ast.js +2 -2
  64. package/dist/esm/cache/ast.js.map +1 -1
  65. package/dist/esm/count-effect.d.ts +42 -0
  66. package/dist/esm/count-effect.d.ts.map +1 -0
  67. package/dist/esm/count-effect.js +53 -0
  68. package/dist/esm/count-effect.js.map +1 -0
  69. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  70. package/dist/esm/fetchers/postgresql.js +2 -1
  71. package/dist/esm/fetchers/postgresql.js.map +1 -1
  72. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  73. package/dist/esm/fetchers/teleport.js +3 -2
  74. package/dist/esm/fetchers/teleport.js.map +1 -1
  75. package/dist/esm/infinite-scroll.d.ts +65 -0
  76. package/dist/esm/infinite-scroll.d.ts.map +1 -0
  77. package/dist/esm/infinite-scroll.js +159 -0
  78. package/dist/esm/infinite-scroll.js.map +1 -0
  79. package/dist/esm/numbered-pagination.d.ts +42 -0
  80. package/dist/esm/numbered-pagination.d.ts.map +1 -0
  81. package/dist/esm/numbered-pagination.js +138 -0
  82. package/dist/esm/numbered-pagination.js.map +1 -0
  83. package/dist/esm/page-reset.d.ts +42 -0
  84. package/dist/esm/page-reset.d.ts.map +1 -0
  85. package/dist/esm/page-reset.js +87 -0
  86. package/dist/esm/page-reset.js.map +1 -0
  87. package/dist/esm/page-url-sync.d.ts +42 -0
  88. package/dist/esm/page-url-sync.d.ts.map +1 -0
  89. package/dist/esm/page-url-sync.js +152 -0
  90. package/dist/esm/page-url-sync.js.map +1 -0
  91. package/dist/esm/pagination-controls.d.ts +55 -0
  92. package/dist/esm/pagination-controls.d.ts.map +1 -0
  93. package/dist/esm/pagination-controls.js +126 -0
  94. package/dist/esm/pagination-controls.js.map +1 -0
  95. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  96. package/dist/esm/pagination-plugin.js +510 -166
  97. package/dist/esm/pagination-plugin.js.map +1 -1
  98. package/dist/esm/product-price-sort.d.ts +116 -0
  99. package/dist/esm/product-price-sort.d.ts.map +1 -0
  100. package/dist/esm/product-price-sort.js +160 -0
  101. package/dist/esm/product-price-sort.js.map +1 -0
  102. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  103. package/dist/esm/transformations/ecommerce-product.js +2 -2
  104. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  105. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  106. package/dist/esm/utils.d.ts.map +1 -1
  107. package/dist/esm/utils.js +6 -8
  108. package/dist/esm/utils.js.map +1 -1
  109. package/package.json +5 -5
  110. package/src/array-mapper-pagination.ts +10 -0
  111. package/src/cache/ast.ts +2 -2
  112. package/src/count-effect.ts +137 -0
  113. package/src/fetchers/postgresql.ts +52 -13
  114. package/src/fetchers/teleport.ts +53 -13
  115. package/src/infinite-scroll.ts +401 -0
  116. package/src/numbered-pagination.ts +292 -0
  117. package/src/page-reset.ts +162 -0
  118. package/src/page-url-sync.ts +241 -0
  119. package/src/pagination-controls.ts +176 -0
  120. package/src/pagination-plugin.ts +758 -351
  121. package/src/product-price-sort.ts +204 -0
  122. package/src/transformations/ecommerce-product.ts +33 -7
  123. package/src/utils.ts +6 -8
  124. package/dist/cjs/array-mapper-registry.d.ts +0 -85
  125. package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
  126. package/dist/cjs/array-mapper-registry.js +0 -296
  127. package/dist/cjs/array-mapper-registry.js.map +0 -1
  128. package/dist/cjs/pagination-with-count.d.ts +0 -6
  129. package/dist/cjs/pagination-with-count.d.ts.map +0 -1
  130. package/dist/cjs/pagination-with-count.js +0 -63
  131. package/dist/cjs/pagination-with-count.js.map +0 -1
  132. package/dist/esm/array-mapper-registry.d.ts +0 -85
  133. package/dist/esm/array-mapper-registry.d.ts.map +0 -1
  134. package/dist/esm/array-mapper-registry.js +0 -292
  135. package/dist/esm/array-mapper-registry.js.map +0 -1
  136. package/dist/esm/pagination-with-count.d.ts +0 -6
  137. package/dist/esm/pagination-with-count.d.ts.map +0 -1
  138. package/dist/esm/pagination-with-count.js +0 -34
  139. package/dist/esm/pagination-with-count.js.map +0 -1
  140. package/src/array-mapper-registry.ts +0 -416
  141. 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 orderClauses = parsedSorts.map((sort) => {
206
- if (!sort.field) return null
207
- const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
208
- return \`\${sort.field} \${order}\`
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
- sql += \` ORDER BY \${orderClauses.join(', ')}\`
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
- sql += \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
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
- sql += \` LIMIT \${limitValue}\`
245
+ sqlTail += \` LIMIT \${limitValue}\`
224
246
  }
225
247
 
226
248
  if (offsetValue !== undefined) {
227
- sql += \` OFFSET \${offsetValue}\`
249
+ sqlTail += \` OFFSET \${offsetValue}\`
228
250
  }
251
+
252
+ const baseSql = sql
253
+ sql = baseSql + orderBySql + sqlTail
229
254
 
230
- const result = await client.query(sql, queryParams)
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
@@ -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 orderClauses = parsedSorts.map((sort) => {
353
- if (!sort.field) return null
354
- const order = (sort.order || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'
355
- return \`\${sort.field} \${order}\`
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
- sql += \` ORDER BY \${orderClauses.join(', ')}\`
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
- sql += \` ORDER BY \${sortBy} \${(sortOrder || '').toUpperCase().startsWith('DESC') ? 'DESC' : 'ASC'}\`
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
- sql += \` LIMIT \${limitValue}\`
393
+ sqlTail += \` LIMIT \${limitValue}\`
371
394
  }
372
395
 
373
396
  if (offsetValue !== undefined) {
374
- sql += \` OFFSET \${offsetValue}\`
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