@lotics/app-sdk 0.48.0 → 0.49.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/dist/src/hooks.js +14 -6
- package/docs/queries.md +43 -25
- package/package.json +1 -1
package/dist/src/hooks.js
CHANGED
|
@@ -101,21 +101,28 @@ export function useInfiniteQuery(alias, params, opts) {
|
|
|
101
101
|
const sort = opts?.sort && opts.sort.length > 0 ? opts.sort : undefined;
|
|
102
102
|
const filter = opts?.filter;
|
|
103
103
|
const mockRows = getMockRows(alias);
|
|
104
|
+
// Keyset (seek) pagination: each page seeks past the previous page's
|
|
105
|
+
// `next_cursor` instead of an increasing OFFSET, so deep scrolls stay O(page)
|
|
106
|
+
// and never skip/duplicate a row as the set shifts. The cursor is opaque; the
|
|
107
|
+
// server keysets a sortable key (or the id default) and falls back to offset
|
|
108
|
+
// transparently. `loadMore`/`rows` are unchanged — the cursor is internal.
|
|
104
109
|
const getKey = (index, prev) => {
|
|
105
110
|
if (mockRows || !enabled)
|
|
106
111
|
return null;
|
|
107
|
-
// Stop once a
|
|
108
|
-
if (index > 0 && (prev == null || prev.
|
|
112
|
+
// Stop once a page reports no next cursor (the end).
|
|
113
|
+
if (index > 0 && (prev == null || prev.next_cursor == null))
|
|
109
114
|
return null;
|
|
110
|
-
|
|
115
|
+
const cursor = index === 0 ? null : (prev?.next_cursor ?? null);
|
|
116
|
+
return ["app-query-infinite", alias, params ?? {}, pageSize, sort ?? null, filter ?? null, cursor];
|
|
111
117
|
};
|
|
112
118
|
const swr = useSWRInfinite(getKey, (key) => {
|
|
113
|
-
const
|
|
119
|
+
const cursor = key[6];
|
|
114
120
|
return rpc("query", {
|
|
115
121
|
alias,
|
|
116
122
|
params: params ?? {},
|
|
117
123
|
limit: pageSize,
|
|
118
|
-
|
|
124
|
+
keyset: true,
|
|
125
|
+
cursor: cursor ?? undefined,
|
|
119
126
|
sort,
|
|
120
127
|
filter,
|
|
121
128
|
});
|
|
@@ -126,7 +133,8 @@ export function useInfiniteQuery(alias, params, opts) {
|
|
|
126
133
|
const pages = (swr.data ?? []).filter((p) => p != null);
|
|
127
134
|
const rows = mockRows ?? pages.flatMap((p) => p.rows ?? []);
|
|
128
135
|
const lastPage = pages.length > 0 ? pages[pages.length - 1] : undefined;
|
|
129
|
-
|
|
136
|
+
// A non-null next_cursor means another page exists; null/absent = the end.
|
|
137
|
+
const hasMore = lastPage != null && lastPage.next_cursor != null;
|
|
130
138
|
const loadingMore = swr.isValidating && swr.size > pages.length;
|
|
131
139
|
const refetch = useCallback(() => {
|
|
132
140
|
void swr.mutate();
|
package/docs/queries.md
CHANGED
|
@@ -246,17 +246,21 @@ addressing is dropped.
|
|
|
246
246
|
{ "kind": "window", "from": { … },
|
|
247
247
|
"partition_by": ["customer_id"],
|
|
248
248
|
"order_by": [{ "field_key": "created", "order": "asc" }],
|
|
249
|
-
"frame": { "type": "rows", "following": 0 }, // optional
|
|
249
|
+
"frame": { "type": "rows", "following": 0 }, // optional; frames `aggregates` only
|
|
250
250
|
"aggregates": [{ "output": "running_total", "type": "number",
|
|
251
|
-
"operation": "sum", "input_column": "total" }]
|
|
251
|
+
"operation": "sum", "input_column": "total" }],
|
|
252
|
+
"functions": [{ "output": "rnk", "fn": "rank" }] } // ranking / navigation (§8)
|
|
252
253
|
```
|
|
253
254
|
|
|
254
|
-
Appends
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
with
|
|
255
|
+
Appends columns to every input row (input columns pass through) from two independent lists:
|
|
256
|
+
**`aggregates`** (frame aggregates — the OVER-legal operation subset, §8) and **`functions`**
|
|
257
|
+
(ranking / navigation: `row_number`, `rank`, `dense_rank`, `percent_rank`, `cume_dist`,
|
|
258
|
+
`ntile`, `lag`, `lead` — §8). At least one list must be non-empty; both may be present. `frame`
|
|
259
|
+
is `rows`-type only (`preceding`/`following` omitted = unbounded, `0` = current row, `N` = N
|
|
260
|
+
rows) and applies **only** to `aggregates`; omitting it uses the SQL default (with `order_by`, a
|
|
261
|
+
*running* frame partition start → current row, ties included; without, the whole partition).
|
|
262
|
+
`functions` are never framed and need a non-empty `order_by`. Output names must not collide with
|
|
263
|
+
input columns.
|
|
260
264
|
|
|
261
265
|
### `sort` / `limit` — order and page a derived set
|
|
262
266
|
|
|
@@ -633,17 +637,30 @@ Sort by the bucket column for a time series; filter it with date operators for a
|
|
|
633
637
|
window. **Always bucket server-side** — shipping raw rows to bucket in JS burns the row cap and
|
|
634
638
|
the timeout for nothing.
|
|
635
639
|
|
|
636
|
-
|
|
640
|
+
A `window` node carries two independent column lists — **`aggregates`** (frame aggregates) and
|
|
641
|
+
**`functions`** (ranking / navigation). At least one must be non-empty; a node may carry both.
|
|
637
642
|
|
|
638
|
-
Only operations that compile to a single
|
|
639
|
-
**`count`, `sum`, `avg`, `min`, `max`, `earliest`, `latest`,
|
|
640
|
-
`unchecked`.** The rest cannot take an OVER clause (`median` is an
|
|
641
|
-
`unique`/`percent_unique` need DISTINCT; `range`/`empty`/`date_range
|
|
642
|
-
multiple calls) — rejected at deploy.
|
|
643
|
+
**`aggregates` — the OVER-legal aggregate subset.** Only operations that compile to a single
|
|
644
|
+
legal SQL window call are accepted: **`count`, `sum`, `avg`, `min`, `max`, `earliest`, `latest`,
|
|
645
|
+
`filled`, `checked`, `unchecked`.** The rest cannot take an OVER clause (`median` is an
|
|
646
|
+
ordered-set aggregate; `unique`/`percent_unique` need DISTINCT; `range`/`empty`/`date_range`/
|
|
647
|
+
`percent_*` compose multiple calls) — rejected at deploy. The optional `frame` applies **only**
|
|
648
|
+
to these; a `frame` on a window with no `aggregates` is rejected as dead config.
|
|
643
649
|
|
|
644
|
-
|
|
645
|
-
`
|
|
646
|
-
|
|
650
|
+
**`functions` — ranking / navigation.** Each is `{ "output", "fn", … }` (its own arg shape, no
|
|
651
|
+
`operation`/`type` — the output type is fixed or inherited). They never take a `frame` and
|
|
652
|
+
**require a non-empty `order_by`** (ranking without an order is nondeterministic):
|
|
653
|
+
|
|
654
|
+
| `fn` | args | output |
|
|
655
|
+
| --- | --- | --- |
|
|
656
|
+
| `row_number` | — | `number`, non-null. Total order 1..N within the partition; ties broken arbitrarily. |
|
|
657
|
+
| `rank` | — | `number`, non-null. Ties share a rank; the next rank **skips** (1,2,2,4). |
|
|
658
|
+
| `dense_rank` | — | `number`, non-null. Ties share a rank; the next rank does **not** skip (1,2,2,3). |
|
|
659
|
+
| `percent_rank` / `cume_dist` | — | `number`, non-null. Relative position in [0, 1]. |
|
|
660
|
+
| `ntile` | `buckets` (positive int) | `number`, non-null. The row's bucket (1..buckets) splitting the partition into equal groups. |
|
|
661
|
+
| `lag` / `lead` | `input_column`, `offset?` (int ≥ 1, default 1), `default?` (literal) | the input column's type, **nullable**. The value `offset` rows before / after this one; at the partition edge, `default` if given else NULL. `default`'s type must match the input column (checked at deploy). |
|
|
662
|
+
|
|
663
|
+
**Top-N per group** — rank within each partition, then filter on the derived rank column:
|
|
647
664
|
|
|
648
665
|
```jsonc
|
|
649
666
|
{ "kind": "filter",
|
|
@@ -651,14 +668,15 @@ frame:
|
|
|
651
668
|
"from": { "kind": "from_table", "table_id": "tbl_orders" },
|
|
652
669
|
"partition_by": ["customer_id"],
|
|
653
670
|
"order_by": [{ "field_key": "total", "order": "desc" }],
|
|
654
|
-
"
|
|
655
|
-
|
|
656
|
-
"predicate": { "node_type": "condition", "field_key": "rank",
|
|
671
|
+
"functions": [{ "output": "rnk", "fn": "rank" }] },
|
|
672
|
+
"predicate": { "node_type": "condition", "field_key": "rnk",
|
|
657
673
|
"operator": "less_than_or_equal_to", "value": 3 } }
|
|
658
674
|
```
|
|
659
675
|
|
|
660
|
-
`
|
|
661
|
-
|
|
676
|
+
`rank` keeps ties (a 3-way tie for 3rd returns all three). For exactly N rows regardless of
|
|
677
|
+
ties, use `row_number` (with a total `order_by` — add a tiebreaker key for determinism).
|
|
678
|
+
**Delta vs the previous row/period** is `lag` (e.g. a project column `{ "expression": "input.total
|
|
679
|
+
- input.prev_total" }` over a `lag` output).
|
|
662
680
|
|
|
663
681
|
### unpivot vs unnest
|
|
664
682
|
|
|
@@ -885,9 +903,9 @@ Consolidated from the sections above — these describe present engine behavior:
|
|
|
885
903
|
|
|
886
904
|
- **No pivot/crosstab node.** Row-values-to-columns happens client-side over grouped results
|
|
887
905
|
(§8).
|
|
888
|
-
- **No
|
|
889
|
-
|
|
890
|
-
subset.
|
|
906
|
+
- **No `first_value`/`last_value`/`nth_value` navigation** — the `functions` set is `row_number`,
|
|
907
|
+
`rank`, `dense_rank`, `percent_rank`, `cume_dist`, `ntile`, `lag`, `lead` (§8). Frame aggregates
|
|
908
|
+
remain the 10-item OVER-legal subset.
|
|
891
909
|
- **Offset-only pagination.** Deep pages cost the full skipped prefix; pages can shift under
|
|
892
910
|
concurrent writes (§9).
|
|
893
911
|
- **No collation control.** Text ORDER BY uses the database default collation —
|