@teleporthq/teleport-plugin-next-data-source 0.43.74 → 0.44.0
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 +95 -5
- package/__tests__/blog-adjacent-arranged.test.ts +176 -0
- package/__tests__/blog-post-contents.test.ts +212 -0
- package/__tests__/blog-post-page-enrichment.test.ts +28 -5
- package/__tests__/blog-post-picture-stand-in.test.ts +48 -0
- package/__tests__/blog-post-reading-time.test.ts +55 -0
- package/__tests__/booking-item-transform.test.ts +120 -0
- package/__tests__/browser-row-policy.test.ts +13 -3
- package/__tests__/content-preset-wiring.test.ts +108 -0
- package/__tests__/fetchers.test.ts +1 -1
- package/__tests__/fixed-list-sort.test.ts +183 -0
- package/__tests__/infinite-scroll.test.ts +154 -5
- package/__tests__/list-sort-order.test.ts +61 -0
- package/__tests__/null-filter-where.test.ts +132 -0
- package/__tests__/pagination-filter-groups.test.ts +186 -0
- package/__tests__/pg-date-parser.test.ts +73 -0
- package/__tests__/plain-list-sort.test.ts +215 -0
- package/__tests__/product-price-sort.ts +58 -25
- package/__tests__/range-control.test.ts +159 -0
- package/__tests__/rest-api-key-header.test.ts +18 -0
- package/__tests__/search-input-marker.test.ts +86 -0
- package/__tests__/search-url-sync.test.ts +61 -16
- package/__tests__/shared-resource-prefetch.test.ts +65 -0
- package/__tests__/table-read-route-protected-tables.test.ts +33 -3
- 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/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 +4 -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/rest-api.d.ts.map +1 -1
- package/dist/cjs/fetchers/rest-api.js +3 -1
- package/dist/cjs/fetchers/rest-api.js.map +1 -1
- package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
- package/dist/cjs/fetchers/teleport.js +5 -2
- package/dist/cjs/fetchers/teleport.js.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/cjs/fetchers/utils/browser-row-policy.js +7 -33
- package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/cjs/filter-utils.d.ts +19 -7
- package/dist/cjs/filter-utils.d.ts.map +1 -1
- package/dist/cjs/filter-utils.js +49 -13
- package/dist/cjs/filter-utils.js.map +1 -1
- package/dist/cjs/infinite-scroll.d.ts +25 -2
- package/dist/cjs/infinite-scroll.d.ts.map +1 -1
- package/dist/cjs/infinite-scroll.js +144 -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/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 +267 -212
- package/dist/cjs/pagination-plugin.js.map +1 -1
- package/dist/cjs/product-price-sort.d.ts +0 -9
- package/dist/cjs/product-price-sort.d.ts.map +1 -1
- package/dist/cjs/product-price-sort.js +1 -13
- package/dist/cjs/product-price-sort.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 +20 -0
- package/dist/cjs/row-order.d.ts.map +1 -0
- package/dist/cjs/row-order.js +46 -0
- package/dist/cjs/row-order.js.map +1 -0
- 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 +10 -0
- package/dist/cjs/sort-utils.d.ts.map +1 -1
- package/dist/cjs/sort-utils.js +22 -1
- package/dist/cjs/sort-utils.js.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/cjs/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js +14 -1
- package/dist/cjs/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts +1 -1
- package/dist/cjs/transformations/blog-comments.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-comments.js +1 -1
- package/dist/cjs/transformations/blog-comments.js.map +1 -1
- package/dist/cjs/transformations/blog-post.d.ts +14 -0
- package/dist/cjs/transformations/blog-post.d.ts.map +1 -1
- package/dist/cjs/transformations/blog-post.js +4 -1
- package/dist/cjs/transformations/blog-post.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/heading-anchors.d.ts +32 -8
- package/dist/cjs/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/cjs/transformations/heading-anchors.js +34 -10
- package/dist/cjs/transformations/heading-anchors.js.map +1 -1
- package/dist/cjs/transformations/index.d.ts +27 -13
- package/dist/cjs/transformations/index.d.ts.map +1 -1
- package/dist/cjs/transformations/index.js +73 -16
- package/dist/cjs/transformations/index.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 +52 -3
- package/dist/cjs/utils.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/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 +5 -2
- 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/rest-api.d.ts.map +1 -1
- package/dist/esm/fetchers/rest-api.js +3 -1
- package/dist/esm/fetchers/rest-api.js.map +1 -1
- package/dist/esm/fetchers/teleport.d.ts.map +1 -1
- package/dist/esm/fetchers/teleport.js +6 -3
- package/dist/esm/fetchers/teleport.js.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts +7 -6
- package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
- package/dist/esm/fetchers/utils/browser-row-policy.js +5 -31
- package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
- package/dist/esm/filter-utils.d.ts +19 -7
- package/dist/esm/filter-utils.d.ts.map +1 -1
- package/dist/esm/filter-utils.js +46 -12
- package/dist/esm/filter-utils.js.map +1 -1
- package/dist/esm/infinite-scroll.d.ts +25 -2
- package/dist/esm/infinite-scroll.d.ts.map +1 -1
- package/dist/esm/infinite-scroll.js +142 -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/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 +271 -216
- package/dist/esm/pagination-plugin.js.map +1 -1
- package/dist/esm/product-price-sort.d.ts +0 -9
- package/dist/esm/product-price-sort.d.ts.map +1 -1
- package/dist/esm/product-price-sort.js +0 -11
- package/dist/esm/product-price-sort.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 +20 -0
- package/dist/esm/row-order.d.ts.map +1 -0
- package/dist/esm/row-order.js +41 -0
- package/dist/esm/row-order.js.map +1 -0
- 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 +10 -0
- package/dist/esm/sort-utils.d.ts.map +1 -1
- package/dist/esm/sort-utils.js +20 -0
- package/dist/esm/sort-utils.js.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts +11 -1
- package/dist/esm/transformations/blog-adjacent-posts.d.ts.map +1 -1
- package/dist/esm/transformations/blog-adjacent-posts.js +14 -1
- package/dist/esm/transformations/blog-adjacent-posts.js.map +1 -1
- package/dist/esm/transformations/blog-comments.d.ts +1 -1
- package/dist/esm/transformations/blog-comments.d.ts.map +1 -1
- package/dist/esm/transformations/blog-comments.js +1 -1
- package/dist/esm/transformations/blog-comments.js.map +1 -1
- package/dist/esm/transformations/blog-post.d.ts +14 -0
- package/dist/esm/transformations/blog-post.d.ts.map +1 -1
- package/dist/esm/transformations/blog-post.js +4 -1
- package/dist/esm/transformations/blog-post.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/heading-anchors.d.ts +32 -8
- package/dist/esm/transformations/heading-anchors.d.ts.map +1 -1
- package/dist/esm/transformations/heading-anchors.js +33 -9
- package/dist/esm/transformations/heading-anchors.js.map +1 -1
- package/dist/esm/transformations/index.d.ts +27 -13
- package/dist/esm/transformations/index.d.ts.map +1 -1
- package/dist/esm/transformations/index.js +72 -16
- package/dist/esm/transformations/index.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 +52 -3
- package/dist/esm/utils.js.map +1 -1
- package/package.json +5 -5
- package/src/count-effect.ts +31 -8
- package/src/fetchers/pg-client-import.ts +16 -0
- package/src/fetchers/postgresql.ts +25 -23
- package/src/fetchers/raw-query.ts +2 -1
- package/src/fetchers/rest-api.ts +3 -1
- package/src/fetchers/teleport.ts +22 -23
- package/src/fetchers/utils/browser-row-policy.ts +10 -20
- package/src/filter-utils.ts +102 -28
- package/src/infinite-scroll.ts +240 -79
- package/src/list-sort-order.ts +34 -0
- package/src/pagination-controls.ts +26 -3
- package/src/pagination-plugin.ts +370 -306
- package/src/product-price-sort.ts +0 -11
- package/src/range-control.ts +207 -0
- package/src/row-order.ts +55 -0
- package/src/search-input.ts +190 -0
- package/src/sort-utils.ts +25 -0
- package/src/transformations/blog-adjacent-posts.ts +89 -3
- package/src/transformations/blog-comments.ts +5 -5
- package/src/transformations/blog-post.ts +64 -13
- package/src/transformations/booking-item.ts +71 -0
- package/src/transformations/heading-anchors.ts +79 -16
- package/src/transformations/index.ts +104 -14
- package/src/utils.ts +74 -3
package/src/filter-utils.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as types from '@babel/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* One entry of the filter TREE the inspector authors: a condition leaf, or a
|
|
5
|
+
* group of further entries joined with `and` / `or`. The generated fetchers
|
|
6
|
+
* read this same tree (`normalizeFilterTree` in utils.ts), so the group
|
|
7
|
+
* operator must reach them intact — flattening an `or` group into its leaves
|
|
8
|
+
* turns "any of" into "all of" on the published site.
|
|
9
|
+
*/
|
|
10
|
+
export type FilterCondition = { source?: string; operand?: string; destination: unknown }
|
|
11
|
+
export type FilterGroup = { type: 'group'; operator?: string; children: FilterEntry[] }
|
|
12
|
+
export type FilterEntry = FilterCondition | FilterGroup
|
|
13
|
+
|
|
14
|
+
export const isFilterGroup = (entry: unknown): entry is FilterGroup =>
|
|
15
|
+
!!entry &&
|
|
16
|
+
typeof entry === 'object' &&
|
|
17
|
+
(entry as { type?: string }).type === 'group' &&
|
|
18
|
+
Array.isArray((entry as { children?: unknown }).children)
|
|
19
|
+
|
|
3
20
|
// Build a single filter entry: { source: '<src>', destination: <destExpr>, operand: '<op>' }
|
|
4
21
|
const buildFilterEntry = (
|
|
5
22
|
source: string,
|
|
@@ -12,10 +29,25 @@ const buildFilterEntry = (
|
|
|
12
29
|
types.objectProperty(types.identifier('operand'), types.stringLiteral(operand)),
|
|
13
30
|
])
|
|
14
31
|
|
|
32
|
+
// { type: 'group', operator: 'and' | 'or', children: <childrenExpr> }
|
|
33
|
+
const buildFilterGroup = (
|
|
34
|
+
operator: string | undefined,
|
|
35
|
+
childrenExpr: types.Expression
|
|
36
|
+
): types.ObjectExpression =>
|
|
37
|
+
types.objectExpression([
|
|
38
|
+
types.objectProperty(types.identifier('type'), types.stringLiteral('group')),
|
|
39
|
+
types.objectProperty(
|
|
40
|
+
types.identifier('operator'),
|
|
41
|
+
types.stringLiteral(operator === 'or' ? 'or' : 'and')
|
|
42
|
+
),
|
|
43
|
+
types.objectProperty(types.identifier('children'), childrenExpr),
|
|
44
|
+
])
|
|
45
|
+
|
|
15
46
|
// Predicate:
|
|
16
|
-
// (__f) => __f.
|
|
17
|
-
// __f.destination !==
|
|
18
|
-
//
|
|
47
|
+
// (__f) => __f.type === 'group' ||
|
|
48
|
+
// (__f.destination !== '' && __f.destination !== null &&
|
|
49
|
+
// __f.destination !== undefined &&
|
|
50
|
+
// !(Array.isArray(__f.destination) && __f.destination.filter(Boolean).length === 0))
|
|
19
51
|
// Used at runtime to drop filter conditions whose destination resolves to an
|
|
20
52
|
// empty value. Keeps the page generic: state-bound filters (e.g.
|
|
21
53
|
// `selectedCategory === ''` after picking "All Categories") and url-bound
|
|
@@ -25,7 +57,9 @@ const buildFilterEntry = (
|
|
|
25
57
|
// string and returns zero rows. The array clause covers array-overlap filters
|
|
26
58
|
// (e.g. category `CATEGORY_DESCENDANTS[selectedCategory] || []`) whose "All"
|
|
27
59
|
// reset resolves to `[]` — an empty overlap set must mean "no filter", not
|
|
28
|
-
// "match nothing".
|
|
60
|
+
// "match nothing". A group has no destination of its own and is kept; its
|
|
61
|
+
// children carry the same `.filter(...)`, and a group emptied that way is
|
|
62
|
+
// dropped by the fetcher's `normalizeFilterTree`.
|
|
29
63
|
const buildNonEmptyDestinationPredicate = (): types.ArrowFunctionExpression => {
|
|
30
64
|
const f = types.identifier('__f')
|
|
31
65
|
const dest = types.memberExpression(f, types.identifier('destination'))
|
|
@@ -49,25 +83,58 @@ const buildNonEmptyDestinationPredicate = (): types.ArrowFunctionExpression => {
|
|
|
49
83
|
return types.arrowFunctionExpression(
|
|
50
84
|
[f],
|
|
51
85
|
types.logicalExpression(
|
|
52
|
-
'
|
|
86
|
+
'||',
|
|
87
|
+
types.binaryExpression(
|
|
88
|
+
'===',
|
|
89
|
+
types.memberExpression(f, types.identifier('type')),
|
|
90
|
+
types.stringLiteral('group')
|
|
91
|
+
),
|
|
53
92
|
types.logicalExpression(
|
|
54
93
|
'&&',
|
|
55
94
|
types.logicalExpression(
|
|
56
95
|
'&&',
|
|
57
|
-
types.
|
|
58
|
-
|
|
96
|
+
types.logicalExpression(
|
|
97
|
+
'&&',
|
|
98
|
+
types.binaryExpression('!==', dest, types.stringLiteral('')),
|
|
99
|
+
types.binaryExpression('!==', dest, types.nullLiteral())
|
|
100
|
+
),
|
|
101
|
+
types.binaryExpression('!==', dest, types.identifier('undefined'))
|
|
59
102
|
),
|
|
60
|
-
types.
|
|
61
|
-
)
|
|
62
|
-
types.unaryExpression('!', isEmptyArray)
|
|
103
|
+
types.unaryExpression('!', isEmptyArray)
|
|
104
|
+
)
|
|
63
105
|
)
|
|
64
106
|
)
|
|
65
107
|
}
|
|
66
108
|
|
|
109
|
+
// `[ entry, ... ].filter(<non-empty destination>)`, recursing into groups so
|
|
110
|
+
// every level of the tree drops its empty conditions at runtime.
|
|
111
|
+
const buildFilteredEntriesArray = (
|
|
112
|
+
filters: FilterEntry[],
|
|
113
|
+
buildDestinationExpression: (destination: unknown) => types.Expression
|
|
114
|
+
): types.CallExpression => {
|
|
115
|
+
const entries = filters.map((filter) =>
|
|
116
|
+
isFilterGroup(filter)
|
|
117
|
+
? buildFilterGroup(
|
|
118
|
+
filter.operator,
|
|
119
|
+
buildFilteredEntriesArray(filter.children, buildDestinationExpression)
|
|
120
|
+
)
|
|
121
|
+
: buildFilterEntry(
|
|
122
|
+
filter.source || '',
|
|
123
|
+
buildDestinationExpression(filter.destination),
|
|
124
|
+
filter.operand || ''
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return types.callExpression(
|
|
129
|
+
types.memberExpression(types.arrayExpression(entries), types.identifier('filter')),
|
|
130
|
+
[buildNonEmptyDestinationPredicate()]
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
67
134
|
// Build the AST for:
|
|
68
135
|
// JSON.stringify(
|
|
69
|
-
// [ {source, destination, operand}, ... ]
|
|
70
|
-
// .filter(__f => __f.destination !== '' && __f.destination !== null && __f.destination !== undefined)
|
|
136
|
+
// [ {source, destination, operand}, { type: 'group', operator, children: [...].filter(...) }, ... ]
|
|
137
|
+
// .filter(__f => __f.type === 'group' || (__f.destination !== '' && __f.destination !== null && __f.destination !== undefined))
|
|
71
138
|
// )
|
|
72
139
|
//
|
|
73
140
|
// The `.filter(...)` step is unconditional even when every entry is a static
|
|
@@ -76,32 +143,39 @@ const buildNonEmptyDestinationPredicate = (): types.ArrowFunctionExpression => {
|
|
|
76
143
|
// type is added but its empty-value behavior is forgotten don't silently
|
|
77
144
|
// re-introduce empty-string filters.
|
|
78
145
|
export const buildFiltersStringifyCall = (
|
|
79
|
-
filters:
|
|
146
|
+
filters: FilterEntry[],
|
|
80
147
|
buildDestinationExpression: (destination: unknown) => types.Expression
|
|
81
|
-
): types.CallExpression =>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
buildDestinationExpression(filter.destination),
|
|
86
|
-
filter.operand || ''
|
|
87
|
-
)
|
|
148
|
+
): types.CallExpression =>
|
|
149
|
+
types.callExpression(
|
|
150
|
+
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
151
|
+
[buildFilteredEntriesArray(filters, buildDestinationExpression)]
|
|
88
152
|
)
|
|
89
153
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
154
|
+
// The build-time counterpart for `getStaticProps`: every destination is a
|
|
155
|
+
// literal (the dynamic ones were pruned — see `pruneFilterTree`), so the tree
|
|
156
|
+
// is written out as plain string literals with no runtime `.filter(...)`.
|
|
157
|
+
const buildStaticFilterEntryAST = (filter: FilterEntry): types.ObjectExpression =>
|
|
158
|
+
isFilterGroup(filter)
|
|
159
|
+
? buildFilterGroup(
|
|
160
|
+
filter.operator,
|
|
161
|
+
types.arrayExpression(filter.children.map(buildStaticFilterEntryAST))
|
|
162
|
+
)
|
|
163
|
+
: buildFilterEntry(
|
|
164
|
+
filter.source || '',
|
|
165
|
+
types.stringLiteral(typeof filter.destination === 'string' ? filter.destination : ''),
|
|
166
|
+
filter.operand || ''
|
|
167
|
+
)
|
|
94
168
|
|
|
95
|
-
|
|
169
|
+
export const buildStaticFiltersStringifyCall = (filters: FilterEntry[]): types.CallExpression =>
|
|
170
|
+
types.callExpression(
|
|
96
171
|
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
97
|
-
[
|
|
172
|
+
[types.arrayExpression(filters.map(buildStaticFilterEntryAST))]
|
|
98
173
|
)
|
|
99
|
-
}
|
|
100
174
|
|
|
101
175
|
// Convenience: push `filters: <call>` onto a paramsProps array.
|
|
102
176
|
export const appendFiltersParam = (
|
|
103
177
|
paramsProps: types.ObjectProperty[],
|
|
104
|
-
filters:
|
|
178
|
+
filters: FilterEntry[] | undefined,
|
|
105
179
|
buildDestinationExpression: (destination: unknown) => types.Expression
|
|
106
180
|
): void => {
|
|
107
181
|
if (!filters || filters.length === 0) {
|
package/src/infinite-scroll.ts
CHANGED
|
@@ -15,6 +15,15 @@ import * as types from '@babel/types'
|
|
|
15
15
|
* and re-enter it. The ref is keyed by a SIGNATURE of every fetch param except
|
|
16
16
|
* the page, so a filter, sort or search change starts a fresh list instead of
|
|
17
17
|
* interleaving rows from two different queries.
|
|
18
|
+
*
|
|
19
|
+
* `fetchData` records the query and the page the list stands on BEFORE it
|
|
20
|
+
* requests them (`latest`), and only the answer to that request changes the
|
|
21
|
+
* list. A filter, sort or search change made past page 1 fires the old page
|
|
22
|
+
* under the new query first, then page 1 once the page is reset: kept, the
|
|
23
|
+
* stray page sat in the list, the page counter said 1, and the next "Load
|
|
24
|
+
* more" fetched a page already on show. `DataProvider` also renders whichever
|
|
25
|
+
* answer arrives LAST, so a late answer to an earlier query must not replace
|
|
26
|
+
* the list either.
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
29
|
// tslint:disable:no-any
|
|
@@ -62,6 +71,7 @@ export const buildInfiniteScrollDeclarations = (
|
|
|
62
71
|
types.objectExpression([
|
|
63
72
|
types.objectProperty(types.identifier('sig'), types.stringLiteral('')),
|
|
64
73
|
types.objectProperty(types.identifier('pages'), types.objectExpression([])),
|
|
74
|
+
types.objectProperty(types.identifier('latest'), types.nullLiteral()),
|
|
65
75
|
]),
|
|
66
76
|
])
|
|
67
77
|
),
|
|
@@ -161,11 +171,56 @@ const writeAtPath = (
|
|
|
161
171
|
])
|
|
162
172
|
}
|
|
163
173
|
|
|
174
|
+
/** `JSON.stringify({ ...params, page: 0 })`: everything that identifies the query but the page. */
|
|
175
|
+
const buildQuerySignature = (): types.CallExpression =>
|
|
176
|
+
types.callExpression(
|
|
177
|
+
types.memberExpression(types.identifier('JSON'), types.identifier('stringify')),
|
|
178
|
+
[
|
|
179
|
+
types.objectExpression([
|
|
180
|
+
types.spreadElement(types.identifier('params')),
|
|
181
|
+
types.objectProperty(types.identifier('page'), types.numericLiteral(0)),
|
|
182
|
+
]),
|
|
183
|
+
]
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
/** `Number(params.page) || 1` */
|
|
187
|
+
const buildRequestedPage = (): types.LogicalExpression =>
|
|
188
|
+
types.logicalExpression(
|
|
189
|
+
'||',
|
|
190
|
+
types.callExpression(types.identifier('Number'), [
|
|
191
|
+
types.memberExpression(types.identifier('params'), types.identifier('page')),
|
|
192
|
+
]),
|
|
193
|
+
types.numericLiteral(1)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
const accumCurrentOf = (vars: InfiniteScrollVars): types.MemberExpression =>
|
|
197
|
+
types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What `fetchData` runs before its request: the query and the page the list
|
|
201
|
+
* now stands on.
|
|
202
|
+
*
|
|
203
|
+
* ds_0_accumRef.current.latest = { sig: JSON.stringify({ ...params, page: 0 }), page: Number(params.page) || 1 }
|
|
204
|
+
*/
|
|
205
|
+
export const buildAccumulatingRequestMark = (vars: InfiniteScrollVars): types.Statement =>
|
|
206
|
+
types.expressionStatement(
|
|
207
|
+
types.assignmentExpression(
|
|
208
|
+
'=',
|
|
209
|
+
types.memberExpression(accumCurrentOf(vars), types.identifier('latest')),
|
|
210
|
+
types.objectExpression([
|
|
211
|
+
types.objectProperty(types.identifier('sig'), buildQuerySignature()),
|
|
212
|
+
types.objectProperty(types.identifier('page'), buildRequestedPage()),
|
|
213
|
+
])
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
164
217
|
/**
|
|
165
218
|
* The `.then(...)` tail that accumulates a page and reports whether more exist.
|
|
166
219
|
*
|
|
167
220
|
* Returned as the resolved value of `fetchData`, so `DataProvider` stores the
|
|
168
|
-
* whole accumulated list as its data
|
|
221
|
+
* whole accumulated list as its data: the pages of the query on show, first
|
|
222
|
+
* to the page it stands on. An answer to anything but the latest request
|
|
223
|
+
* (`buildAccumulatingRequestMark`) is not kept, and gets the list as it is.
|
|
169
224
|
*
|
|
170
225
|
* `itemsPath` is where the rows sit inside a payload that wraps them
|
|
171
226
|
* (`{ items: [...], total }`): the rows are read there and the accumulated list
|
|
@@ -178,10 +233,11 @@ export const buildAccumulatingResponseHandler = (
|
|
|
178
233
|
perPage: number,
|
|
179
234
|
itemsPath?: string[]
|
|
180
235
|
): types.ArrowFunctionExpression => {
|
|
181
|
-
const accumCurrent = (): types.MemberExpression =>
|
|
182
|
-
types.memberExpression(types.identifier(vars.accumRefVar), types.identifier('current'))
|
|
236
|
+
const accumCurrent = (): types.MemberExpression => accumCurrentOf(vars)
|
|
183
237
|
const accumPages = (): types.MemberExpression =>
|
|
184
238
|
types.memberExpression(accumCurrent(), types.identifier('pages'))
|
|
239
|
+
const latestOf = (key: string): types.MemberExpression =>
|
|
240
|
+
types.memberExpression(types.identifier('latest'), types.identifier(key))
|
|
185
241
|
const payload = (): types.OptionalMemberExpression =>
|
|
186
242
|
types.optionalMemberExpression(
|
|
187
243
|
types.identifier('response'),
|
|
@@ -191,6 +247,8 @@ export const buildAccumulatingResponseHandler = (
|
|
|
191
247
|
)
|
|
192
248
|
const rowsPath = itemsPath?.length ? itemsPath : undefined
|
|
193
249
|
|
|
250
|
+
// Every page held up to `last`, flattened in page order — a numeric sort, so
|
|
251
|
+
// page 10 does not land between 1 and 2.
|
|
194
252
|
const accumulatedRows = types.callExpression(
|
|
195
253
|
types.memberExpression(
|
|
196
254
|
types.callExpression(
|
|
@@ -198,12 +256,23 @@ export const buildAccumulatingResponseHandler = (
|
|
|
198
256
|
types.callExpression(
|
|
199
257
|
types.memberExpression(
|
|
200
258
|
types.callExpression(
|
|
201
|
-
types.memberExpression(
|
|
202
|
-
|
|
259
|
+
types.memberExpression(
|
|
260
|
+
types.callExpression(
|
|
261
|
+
types.memberExpression(types.identifier('Object'), types.identifier('keys')),
|
|
262
|
+
[accumPages()]
|
|
263
|
+
),
|
|
264
|
+
types.identifier('map')
|
|
265
|
+
),
|
|
266
|
+
[types.identifier('Number')]
|
|
203
267
|
),
|
|
204
|
-
types.identifier('
|
|
268
|
+
types.identifier('filter')
|
|
205
269
|
),
|
|
206
|
-
[
|
|
270
|
+
[
|
|
271
|
+
types.arrowFunctionExpression(
|
|
272
|
+
[types.identifier('p')],
|
|
273
|
+
types.binaryExpression('<=', types.identifier('p'), types.identifier('last'))
|
|
274
|
+
),
|
|
275
|
+
]
|
|
207
276
|
),
|
|
208
277
|
types.identifier('sort')
|
|
209
278
|
),
|
|
@@ -242,73 +311,97 @@ export const buildAccumulatingResponseHandler = (
|
|
|
242
311
|
),
|
|
243
312
|
]),
|
|
244
313
|
types.variableDeclaration('const', [
|
|
245
|
-
types.variableDeclarator(
|
|
246
|
-
types.identifier('page'),
|
|
247
|
-
types.logicalExpression(
|
|
248
|
-
'||',
|
|
249
|
-
types.callExpression(types.identifier('Number'), [
|
|
250
|
-
types.memberExpression(types.identifier('params'), types.identifier('page')),
|
|
251
|
-
]),
|
|
252
|
-
types.numericLiteral(1)
|
|
253
|
-
)
|
|
254
|
-
),
|
|
314
|
+
types.variableDeclarator(types.identifier('page'), buildRequestedPage()),
|
|
255
315
|
]),
|
|
256
316
|
// Everything that identifies the QUERY, with the page zeroed out: two
|
|
257
317
|
// fetches that differ only by page belong to the same accumulated list.
|
|
318
|
+
types.variableDeclaration('const', [
|
|
319
|
+
types.variableDeclarator(types.identifier('sig'), buildQuerySignature()),
|
|
320
|
+
]),
|
|
321
|
+
// The request the list stands on now, recorded when it was made.
|
|
258
322
|
types.variableDeclaration('const', [
|
|
259
323
|
types.variableDeclarator(
|
|
260
|
-
types.identifier('
|
|
261
|
-
types.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
]
|
|
324
|
+
types.identifier('latest'),
|
|
325
|
+
types.logicalExpression(
|
|
326
|
+
'||',
|
|
327
|
+
types.memberExpression(accumCurrent(), types.identifier('latest')),
|
|
328
|
+
types.objectExpression([
|
|
329
|
+
types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
|
|
330
|
+
types.objectProperty(types.identifier('page'), types.identifier('page'), false, true),
|
|
331
|
+
])
|
|
269
332
|
)
|
|
270
333
|
),
|
|
271
334
|
]),
|
|
335
|
+
// Only its answer is kept: an earlier query's, or a page past the one a
|
|
336
|
+
// filter / sort / search change restarted the list at, changes nothing.
|
|
272
337
|
types.ifStatement(
|
|
273
|
-
types.
|
|
274
|
-
'
|
|
275
|
-
types.
|
|
276
|
-
types.identifier('
|
|
338
|
+
types.logicalExpression(
|
|
339
|
+
'&&',
|
|
340
|
+
types.binaryExpression('===', types.identifier('sig'), latestOf('sig')),
|
|
341
|
+
types.binaryExpression('<=', types.identifier('page'), latestOf('page'))
|
|
277
342
|
),
|
|
278
343
|
types.blockStatement([
|
|
344
|
+
types.ifStatement(
|
|
345
|
+
types.binaryExpression(
|
|
346
|
+
'!==',
|
|
347
|
+
types.memberExpression(accumCurrent(), types.identifier('sig')),
|
|
348
|
+
types.identifier('sig')
|
|
349
|
+
),
|
|
350
|
+
types.blockStatement([
|
|
351
|
+
types.expressionStatement(
|
|
352
|
+
types.assignmentExpression(
|
|
353
|
+
'=',
|
|
354
|
+
types.memberExpression(accumCurrent(), types.identifier('sig')),
|
|
355
|
+
types.identifier('sig')
|
|
356
|
+
)
|
|
357
|
+
),
|
|
358
|
+
types.expressionStatement(
|
|
359
|
+
types.assignmentExpression('=', accumPages(), types.objectExpression([]))
|
|
360
|
+
),
|
|
361
|
+
])
|
|
362
|
+
),
|
|
279
363
|
types.expressionStatement(
|
|
280
364
|
types.assignmentExpression(
|
|
281
365
|
'=',
|
|
282
|
-
|
|
283
|
-
types.
|
|
284
|
-
types.objectProperty(types.identifier('sig'), types.identifier('sig'), false, true),
|
|
285
|
-
types.objectProperty(types.identifier('pages'), types.objectExpression([])),
|
|
286
|
-
])
|
|
366
|
+
types.memberExpression(accumPages(), types.identifier('page'), true),
|
|
367
|
+
types.identifier('rows')
|
|
287
368
|
)
|
|
288
369
|
),
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
types.memberExpression(types.identifier('rows'), types.identifier('length')),
|
|
305
|
-
types.numericLiteral(perPage)
|
|
370
|
+
// A page that came back short is the last one. This is the only
|
|
371
|
+
// end-of-data signal a data source that cannot count ever gives.
|
|
372
|
+
types.ifStatement(
|
|
373
|
+
types.binaryExpression('===', types.identifier('page'), latestOf('page')),
|
|
374
|
+
types.blockStatement([
|
|
375
|
+
types.expressionStatement(
|
|
376
|
+
types.callExpression(types.identifier(vars.setHasMoreFlagVar), [
|
|
377
|
+
types.binaryExpression(
|
|
378
|
+
'>=',
|
|
379
|
+
types.memberExpression(types.identifier('rows'), types.identifier('length')),
|
|
380
|
+
types.numericLiteral(perPage)
|
|
381
|
+
),
|
|
382
|
+
])
|
|
383
|
+
),
|
|
384
|
+
])
|
|
306
385
|
),
|
|
307
386
|
])
|
|
308
387
|
),
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
388
|
+
// The pages of the query on show, up to the page the list stands on; all
|
|
389
|
+
// of them while the latest query has not answered yet.
|
|
390
|
+
types.variableDeclaration('const', [
|
|
391
|
+
types.variableDeclarator(
|
|
392
|
+
types.identifier('last'),
|
|
393
|
+
types.conditionalExpression(
|
|
394
|
+
types.binaryExpression(
|
|
395
|
+
'===',
|
|
396
|
+
types.memberExpression(accumCurrent(), types.identifier('sig')),
|
|
397
|
+
latestOf('sig')
|
|
398
|
+
),
|
|
399
|
+
latestOf('page'),
|
|
400
|
+
types.identifier('Infinity')
|
|
401
|
+
)
|
|
402
|
+
),
|
|
403
|
+
]),
|
|
404
|
+
// This is what DataProvider stores as its data, and so what the list renders.
|
|
312
405
|
types.returnStatement(
|
|
313
406
|
rowsPath ? writeAtPath(payload(), rowsPath, accumulatedRows) : accumulatedRows
|
|
314
407
|
),
|
|
@@ -431,35 +524,103 @@ export const buildSentinelObserverEffect = (
|
|
|
431
524
|
])
|
|
432
525
|
)
|
|
433
526
|
|
|
527
|
+
const styleAttribute = (entries: Record<string, string | number>): types.JSXAttribute =>
|
|
528
|
+
types.jsxAttribute(
|
|
529
|
+
types.jsxIdentifier('style'),
|
|
530
|
+
types.jsxExpressionContainer(
|
|
531
|
+
types.objectExpression(
|
|
532
|
+
Object.entries(entries).map(([key, value]) =>
|
|
533
|
+
types.objectProperty(
|
|
534
|
+
types.identifier(key),
|
|
535
|
+
typeof value === 'number' ? types.numericLiteral(value) : types.stringLiteral(value)
|
|
536
|
+
)
|
|
537
|
+
)
|
|
538
|
+
)
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
const sentinelTag = (
|
|
543
|
+
tag: string,
|
|
544
|
+
attributes: types.JSXAttribute[],
|
|
545
|
+
children: types.JSXElement[] = []
|
|
546
|
+
): types.JSXElement =>
|
|
547
|
+
types.jsxElement(
|
|
548
|
+
types.jsxOpeningElement(types.jsxIdentifier(tag), attributes, children.length === 0),
|
|
549
|
+
children.length === 0 ? null : types.jsxClosingElement(types.jsxIdentifier(tag)),
|
|
550
|
+
children,
|
|
551
|
+
children.length === 0
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
/** Parents whose content is phrasing only: a `<div>` in one closes it in the parser. */
|
|
555
|
+
const PHRASING_PARENTS: ReadonlySet<string> = new Set([
|
|
556
|
+
'p',
|
|
557
|
+
'span',
|
|
558
|
+
'a',
|
|
559
|
+
'button',
|
|
560
|
+
'label',
|
|
561
|
+
'legend',
|
|
562
|
+
'caption',
|
|
563
|
+
'h1',
|
|
564
|
+
'h2',
|
|
565
|
+
'h3',
|
|
566
|
+
'h4',
|
|
567
|
+
'h5',
|
|
568
|
+
'h6',
|
|
569
|
+
'strong',
|
|
570
|
+
'em',
|
|
571
|
+
'b',
|
|
572
|
+
'i',
|
|
573
|
+
'small',
|
|
574
|
+
])
|
|
575
|
+
|
|
434
576
|
/**
|
|
435
577
|
* `<div ref={ds_N_sentinelRef} aria-hidden="true" style={{ height: '1px' }} />`
|
|
436
578
|
*
|
|
437
579
|
* An invisible one-pixel row after the list. It is generated rather than
|
|
438
580
|
* authored because it is not a control — there is nothing to style or click,
|
|
439
581
|
* only a position in the document for the observer to watch.
|
|
582
|
+
*
|
|
583
|
+
* It takes the element its parent may hold: a table section holds rows, a
|
|
584
|
+
* list holds items, a paragraph holds phrasing. A `<div>` among a table's
|
|
585
|
+
* rows is moved out of the table by the parser, so the server's markup and
|
|
586
|
+
* the client's tree differ and React's hydration fails on every such list.
|
|
440
587
|
*/
|
|
441
|
-
export const buildSentinelElement = (
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
),
|
|
450
|
-
types.jsxAttribute(types.jsxIdentifier('aria-hidden'), types.stringLiteral('true')),
|
|
451
|
-
types.jsxAttribute(
|
|
452
|
-
types.jsxIdentifier('style'),
|
|
453
|
-
types.jsxExpressionContainer(
|
|
454
|
-
types.objectExpression([
|
|
455
|
-
types.objectProperty(types.identifier('height'), types.stringLiteral('1px')),
|
|
456
|
-
])
|
|
457
|
-
)
|
|
458
|
-
),
|
|
459
|
-
],
|
|
460
|
-
true
|
|
588
|
+
export const buildSentinelElement = (
|
|
589
|
+
vars: InfiniteScrollVars,
|
|
590
|
+
parentTag: string | null = null
|
|
591
|
+
): types.JSXElement => {
|
|
592
|
+
const observed = [
|
|
593
|
+
types.jsxAttribute(
|
|
594
|
+
types.jsxIdentifier('ref'),
|
|
595
|
+
types.jsxExpressionContainer(types.identifier(vars.sentinelRefVar))
|
|
461
596
|
),
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
597
|
+
types.jsxAttribute(types.jsxIdentifier('aria-hidden'), types.stringLiteral('true')),
|
|
598
|
+
]
|
|
599
|
+
const emptyCell = () => sentinelTag('td', [styleAttribute({ padding: 0, border: 0 })])
|
|
600
|
+
switch (parentTag) {
|
|
601
|
+
case 'tbody':
|
|
602
|
+
case 'thead':
|
|
603
|
+
case 'tfoot':
|
|
604
|
+
return sentinelTag('tr', [...observed, styleAttribute({ height: '1px' })], [emptyCell()])
|
|
605
|
+
case 'table':
|
|
606
|
+
return sentinelTag(
|
|
607
|
+
'tbody',
|
|
608
|
+
[...observed, styleAttribute({ height: '1px' })],
|
|
609
|
+
[sentinelTag('tr', [], [emptyCell()])]
|
|
610
|
+
)
|
|
611
|
+
case 'tr':
|
|
612
|
+
return sentinelTag('td', [...observed, styleAttribute({ height: '1px', padding: 0 })])
|
|
613
|
+
case 'ul':
|
|
614
|
+
case 'ol':
|
|
615
|
+
case 'menu':
|
|
616
|
+
return sentinelTag('li', [...observed, styleAttribute({ height: '1px', listStyle: 'none' })])
|
|
617
|
+
default:
|
|
618
|
+
if (parentTag && PHRASING_PARENTS.has(parentTag)) {
|
|
619
|
+
return sentinelTag('span', [
|
|
620
|
+
...observed,
|
|
621
|
+
styleAttribute({ display: 'block', height: '1px' }),
|
|
622
|
+
])
|
|
623
|
+
}
|
|
624
|
+
return sentinelTag('div', [...observed, styleAttribute({ height: '1px' })])
|
|
625
|
+
}
|
|
626
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PRODUCTS_TABLE_NAME } from './product-price-sort'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a generated list route orders the rows a visitor's sort leaves tied or
|
|
5
|
+
* empty, in the SQL fetchers that serve the platform's listing pages
|
|
6
|
+
* (`teleport.ts`, `postgresql.ts`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The platform tables whose listing pages offer visitors a sort — the
|
|
11
|
+
* products page, the blog listing, the booking page — each with an `id` from
|
|
12
|
+
* its first migration.
|
|
13
|
+
*/
|
|
14
|
+
const LISTING_TABLES_WITH_ID: ReadonlySet<string> = new Set([
|
|
15
|
+
PRODUCTS_TABLE_NAME,
|
|
16
|
+
'teleport_blog_posts',
|
|
17
|
+
'teleport_booking_items',
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether a table is one of those listing tables, so its rows are told apart
|
|
22
|
+
* by `id` even when the project saved no columns for it (see `row-order.ts`).
|
|
23
|
+
*/
|
|
24
|
+
export const isListingTableWithId = (tableName: string): boolean =>
|
|
25
|
+
LISTING_TABLES_WITH_ID.has(tableName)
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Appended to every sorted column: a row without a value comes after the
|
|
29
|
+
* rows with one, whichever the direction. PostgreSQL puts NULLs FIRST in a
|
|
30
|
+
* descending order, so "longest read first" opened on the posts without a
|
|
31
|
+
* reading time. Only on the ORDER BY a fetcher tries first — its plain-column
|
|
32
|
+
* fallback leaves it out, for a database that rejects the clause.
|
|
33
|
+
*/
|
|
34
|
+
export const SORT_NULLS_LAST_SQL = ' NULLS LAST'
|