@ultimat3/query 22.11.0 → 22.12.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/README.md +8 -5
- package/package.json +7 -7
- package/src/http.ts +2 -1
- package/src/openapi.ts +11 -4
- package/src/page-controls.ts +1 -1
- package/src/pagination.ts +31 -14
package/README.md
CHANGED
|
@@ -336,16 +336,19 @@ not exported, because a page is the read's own answer rather than an imported he
|
|
|
336
336
|
### The same page over HTTP — `?_first=` and `?_after=`
|
|
337
337
|
|
|
338
338
|
The route reads two controls off the search string BEFORE the schema sees it, and with either
|
|
339
|
-
present answers the `Page` envelope `.page()` answers a server caller with — `{ rows,
|
|
340
|
-
hasNextPage }`, the same names, so a cursor read off the wire and one read off a
|
|
341
|
-
one string in one field.
|
|
339
|
+
present answers the `Page` envelope `.page()` answers a server caller with — `{ rows, nextCursor,
|
|
340
|
+
hasMore, endCursor, hasNextPage }`, the same names, so a cursor read off the wire and one read off a
|
|
341
|
+
direct call are one string in one field. **`nextCursor` and `hasMore` are the preferred names**
|
|
342
|
+
(`As of 22.12`); `endCursor` and `hasNextPage` carry the same two values, always, as aliases — not
|
|
343
|
+
deprecated, so a client reading them keeps working. `openapi.json` lists all five as required, the
|
|
344
|
+
aliases described as such. Without a control the answer is the bare array it has always been, so a
|
|
342
345
|
client written before the controls existed keeps reading rows. `As of 2026-09`; until then the
|
|
343
346
|
route had no envelope, and a query over a paged external source carried its page marker on a row
|
|
344
347
|
(an `olderCursor` column on the oldest message), which is a cursor on the wrong side of the shape.
|
|
345
348
|
|
|
346
349
|
```
|
|
347
|
-
GET /_x/query/live-feed?orgId=…&_first=20 → { rows, endCursor, hasNextPage }
|
|
348
|
-
GET /_x/query/live-feed?orgId=…&_first=20&_after=<c> → the next page
|
|
350
|
+
GET /_x/query/live-feed?orgId=…&_first=20 → { rows, nextCursor, hasMore, endCursor, hasNextPage }
|
|
351
|
+
GET /_x/query/live-feed?orgId=…&_first=20&_after=<c> → the next page (<c> = nextCursor)
|
|
349
352
|
GET /_x/query/live-feed?orgId=… → [ …rows ] (unchanged)
|
|
350
353
|
```
|
|
351
354
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/query",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.12.0",
|
|
4
4
|
"description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"test": "bun test"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ultimat3/cache": "22.
|
|
40
|
-
"@ultimat3/core": "22.
|
|
41
|
-
"@ultimat3/entity": "22.
|
|
42
|
-
"@ultimat3/http": "22.
|
|
43
|
-
"@ultimat3/policy": "22.
|
|
44
|
-
"@ultimat3/schema": "22.
|
|
39
|
+
"@ultimat3/cache": "22.12.0",
|
|
40
|
+
"@ultimat3/core": "22.12.0",
|
|
41
|
+
"@ultimat3/entity": "22.12.0",
|
|
42
|
+
"@ultimat3/http": "22.12.0",
|
|
43
|
+
"@ultimat3/policy": "22.12.0",
|
|
44
|
+
"@ultimat3/schema": "22.12.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/http.ts
CHANGED
|
@@ -80,7 +80,8 @@ export function toQueryRoute(target: AnyQuery): Route {
|
|
|
80
80
|
return answerRow(row);
|
|
81
81
|
}
|
|
82
82
|
// With a page control the answer is the `Page` envelope `query.page()` answers a server
|
|
83
|
-
// caller with — `{ rows, endCursor, hasNextPage }`, the same names
|
|
83
|
+
// caller with — `{ rows, nextCursor, hasMore, endCursor, hasNextPage }`, the same names (the
|
|
84
|
+
// last two aliases of the first two), so a cursor read off the
|
|
84
85
|
// wire and one read off a direct call are the same string in the same field. Without one the
|
|
85
86
|
// answer is the bare array it has always been: every client written before the controls
|
|
86
87
|
// existed keeps reading rows, and `hasNextPage` never rides on a row where a page marker
|
package/src/openapi.ts
CHANGED
|
@@ -48,11 +48,18 @@ const ANSWERS: readonly Record<string, unknown>[] = [
|
|
|
48
48
|
{ type: 'array', items: {} },
|
|
49
49
|
{
|
|
50
50
|
type: 'object',
|
|
51
|
-
|
|
51
|
+
// `nextCursor`/`hasMore` are the preferred names; the other two are the same values, kept as
|
|
52
|
+
// aliases — documented as such, not deprecated — so every client written against them reads on.
|
|
53
|
+
required: ['rows', 'nextCursor', 'hasMore', 'endCursor', 'hasNextPage'],
|
|
52
54
|
properties: {
|
|
53
55
|
rows: { type: 'array', items: {} },
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
nextCursor: {
|
|
57
|
+
type: ['string', 'null'],
|
|
58
|
+
description: `the signed cursor that continues this listing — send it as ${PAGE_AFTER_KEY}; null on an empty page`,
|
|
59
|
+
},
|
|
60
|
+
hasMore: { type: 'boolean', description: 'whether another page follows' },
|
|
61
|
+
endCursor: { type: ['string', 'null'], description: 'alias of nextCursor — the same value' },
|
|
62
|
+
hasNextPage: { type: 'boolean', description: 'alias of hasMore — the same value' },
|
|
56
63
|
},
|
|
57
64
|
},
|
|
58
65
|
];
|
|
@@ -170,7 +177,7 @@ const PAGE_PARAMETERS: readonly Record<string, unknown>[] = [
|
|
|
170
177
|
name: PAGE_AFTER_KEY,
|
|
171
178
|
in: 'query',
|
|
172
179
|
required: false,
|
|
173
|
-
description: `the endCursor a previous page answered; needs ${PAGE_FIRST_KEY}`,
|
|
180
|
+
description: `the nextCursor (alias endCursor) a previous page answered; needs ${PAGE_FIRST_KEY}`,
|
|
174
181
|
schema: { type: 'string' },
|
|
175
182
|
},
|
|
176
183
|
];
|
package/src/page-controls.ts
CHANGED
|
@@ -90,7 +90,7 @@ export function pageControlsOf(name: string, values: SearchValues): SplitSearch
|
|
|
90
90
|
if (cursor.length === 0) {
|
|
91
91
|
throw new QueryInputInvalidError(
|
|
92
92
|
name,
|
|
93
|
-
`${PAGE_AFTER_KEY} is empty — omit it for the first page, or send the
|
|
93
|
+
`${PAGE_AFTER_KEY} is empty — omit it for the first page, or send the nextCursor the previous page answered`,
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
return { input, page: { first: Number(size), after: cursor } };
|
package/src/pagination.ts
CHANGED
|
@@ -20,12 +20,32 @@ import { compareRows, seekKeyOf, totalOrder } from './shape';
|
|
|
20
20
|
import type { SqlSource } from './source';
|
|
21
21
|
import { isAfterKey } from './source';
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* One page. `nextCursor` / `hasMore` are the preferred names; `endCursor` / `hasNextPage` are the
|
|
25
|
+
* same two values under the names every client before 22.12 reads — aliases, both always present,
|
|
26
|
+
* never able to disagree because `pageOf` is the one place that writes them.
|
|
27
|
+
*/
|
|
23
28
|
export interface Page<TRow> {
|
|
24
29
|
readonly rows: readonly TRow[];
|
|
30
|
+
/** The signed cursor that continues this listing; `null` on an empty page. */
|
|
31
|
+
readonly nextCursor: string | null;
|
|
32
|
+
/** Whether another page follows. */
|
|
33
|
+
readonly hasMore: boolean;
|
|
34
|
+
/** Alias of `nextCursor`. */
|
|
25
35
|
readonly endCursor: string | null;
|
|
36
|
+
/** Alias of `hasMore`. */
|
|
26
37
|
readonly hasNextPage: boolean;
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
/** Builds a `Page` from its two facts — the only constructor, so the aliases cannot drift. */
|
|
41
|
+
export function pageOf<TRow>(
|
|
42
|
+
rows: readonly TRow[],
|
|
43
|
+
nextCursor: string | null,
|
|
44
|
+
hasMore: boolean,
|
|
45
|
+
): Page<TRow> {
|
|
46
|
+
return { rows, nextCursor, hasMore, endCursor: nextCursor, hasNextPage: hasMore };
|
|
47
|
+
}
|
|
48
|
+
|
|
29
49
|
export interface PaginateArgs extends SourceOptions {
|
|
30
50
|
readonly first: number;
|
|
31
51
|
readonly after?: string;
|
|
@@ -70,20 +90,17 @@ export async function paginate<TInput extends StandardSchemaV1, TRow extends obj
|
|
|
70
90
|
const last = rows[rows.length - 1];
|
|
71
91
|
const seek = last === undefined ? null : seekKeyOf(last, shape);
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}),
|
|
85
|
-
hasNextPage: scoped.length > args.first,
|
|
86
|
-
};
|
|
93
|
+
const nextCursor =
|
|
94
|
+
seek === null
|
|
95
|
+
? null
|
|
96
|
+
: encodeCursor({
|
|
97
|
+
scope: hash,
|
|
98
|
+
// The id rides TYPED at the tail of the key — core's `id` slot is a string — so a
|
|
99
|
+
// numeric or bigint tiebreak compares as a number on the next page, not lexically.
|
|
100
|
+
key: [...seek.key, seek.id].map(serializeSortValue),
|
|
101
|
+
id: String(seek.id),
|
|
102
|
+
});
|
|
103
|
+
return pageOf(rows, nextCursor, scoped.length > args.first);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
/**
|