@daloyjs/core 0.36.0 → 0.38.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/LICENSE +21 -0
- package/README.md +34 -3
- package/bin/daloy.mjs +2 -0
- package/dist/adapters/bun.js +16 -9
- package/dist/adapters/deno.js +7 -1
- package/dist/adapters/node.d.ts +25 -0
- package/dist/adapters/node.js +32 -0
- package/dist/app.d.ts +200 -6
- package/dist/app.js +235 -50
- package/dist/asyncapi.d.ts +98 -0
- package/dist/asyncapi.js +212 -0
- package/dist/auto-ban.d.ts +205 -0
- package/dist/auto-ban.js +222 -0
- package/dist/bot-guard.d.ts +209 -0
- package/dist/bot-guard.js +291 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +113 -4
- package/dist/client.d.ts +23 -0
- package/dist/client.js +16 -0
- package/dist/concurrency-limit.d.ts +135 -0
- package/dist/concurrency-limit.js +254 -0
- package/dist/docs.d.ts +57 -6
- package/dist/docs.js +34 -3
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +57 -0
- package/dist/fetch-guard.js +4 -0
- package/dist/fetch-resilience.d.ts +295 -0
- package/dist/fetch-resilience.js +485 -0
- package/dist/geo-block.d.ts +184 -0
- package/dist/geo-block.js +153 -0
- package/dist/hashing.d.ts +2 -1
- package/dist/hashing.js +12 -1
- package/dist/http-signatures.d.ts +303 -0
- package/dist/http-signatures.js +782 -0
- package/dist/idempotency.d.ts +204 -0
- package/dist/idempotency.js +341 -0
- package/dist/index.d.ts +39 -5
- package/dist/index.js +19 -2
- package/dist/ip-reputation.d.ts +198 -0
- package/dist/ip-reputation.js +253 -0
- package/dist/jwk.d.ts +15 -0
- package/dist/jwk.js +24 -2
- package/dist/load-shedding.d.ts +5 -0
- package/dist/logger.js +6 -2
- package/dist/metrics.d.ts +208 -0
- package/dist/metrics.js +452 -0
- package/dist/middleware.js +0 -10
- package/dist/mtls.d.ts +266 -0
- package/dist/mtls.js +488 -0
- package/dist/multipart.js +1 -1
- package/dist/openapi-diff.d.ts +79 -0
- package/dist/openapi-diff.js +246 -0
- package/dist/openapi.js +4 -1
- package/dist/pagination.d.ts +210 -0
- package/dist/pagination.js +353 -0
- package/dist/rate-limit-redis.d.ts +8 -0
- package/dist/rate-limit-redis.js +8 -0
- package/dist/request-decompression.d.ts +200 -0
- package/dist/request-decompression.js +363 -0
- package/dist/response-cache.d.ts +205 -0
- package/dist/response-cache.js +374 -0
- package/dist/router.d.ts +22 -0
- package/dist/router.js +64 -7
- package/dist/safe-redirect.d.ts +2 -2
- package/dist/safe-redirect.js +3 -8
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/scheduler.d.ts +315 -0
- package/dist/scheduler.js +546 -0
- package/dist/security.d.ts +61 -7
- package/dist/security.js +75 -8
- package/dist/session.js +3 -3
- package/dist/types.d.ts +33 -0
- package/dist/waf.d.ts +213 -0
- package/dist/waf.js +334 -0
- package/dist/webhook-delivery.d.ts +263 -0
- package/dist/webhook-delivery.js +311 -0
- package/dist/websocket.d.ts +52 -0
- package/dist/websocket.js +13 -0
- package/package.json +79 -3
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@daloyjs/core/openapi-diff` — pure, dependency-free OpenAPI 3.x diffing.
|
|
3
|
+
*
|
|
4
|
+
* Compares two OpenAPI documents (a published *baseline* and a freshly
|
|
5
|
+
* generated *current* spec) and classifies every structural change as either
|
|
6
|
+
* **breaking** (a consumer relying on the baseline could now fail) or
|
|
7
|
+
* **non-breaking** (purely additive / informational). This is the engine
|
|
8
|
+
* behind the `daloy diff` CLI command and the `verify:breaking-changes` CI
|
|
9
|
+
* gate, answering the single question a contract-first framework should make
|
|
10
|
+
* trivial: *"did this change break my published API?"*
|
|
11
|
+
*
|
|
12
|
+
* The implementation walks plain JSON and never imports a schema validator or
|
|
13
|
+
* any runtime dependency, so it can run in any environment that can read two
|
|
14
|
+
* JSON files.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
* @since 0.37.0
|
|
18
|
+
*/
|
|
19
|
+
/** HTTP methods recognized on an OpenAPI Path Item Object. */
|
|
20
|
+
const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
|
|
21
|
+
/** Narrow an unknown value to a plain object (non-null, non-array). */
|
|
22
|
+
function isObject(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
/** Extract the `paths` object from a document, tolerating malformed input. */
|
|
26
|
+
function pathsOf(doc) {
|
|
27
|
+
if (isObject(doc) && isObject(doc.paths))
|
|
28
|
+
return doc.paths;
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
/** Extract the OpenAPI Operation Objects keyed by HTTP method for a path item. */
|
|
32
|
+
function operationsOf(pathItem) {
|
|
33
|
+
const ops = new Map();
|
|
34
|
+
if (!isObject(pathItem))
|
|
35
|
+
return ops;
|
|
36
|
+
for (const method of HTTP_METHODS) {
|
|
37
|
+
const op = pathItem[method];
|
|
38
|
+
if (isObject(op))
|
|
39
|
+
ops.set(method, op);
|
|
40
|
+
}
|
|
41
|
+
return ops;
|
|
42
|
+
}
|
|
43
|
+
/** Build a `${in}:${name}` keyed map of an operation's parameter objects. */
|
|
44
|
+
function parametersOf(op) {
|
|
45
|
+
const out = new Map();
|
|
46
|
+
const params = op.parameters;
|
|
47
|
+
if (!Array.isArray(params))
|
|
48
|
+
return out;
|
|
49
|
+
for (const p of params) {
|
|
50
|
+
if (!isObject(p))
|
|
51
|
+
continue;
|
|
52
|
+
const name = typeof p.name === "string" ? p.name : "";
|
|
53
|
+
const loc = typeof p.in === "string" ? p.in : "";
|
|
54
|
+
if (name === "")
|
|
55
|
+
continue;
|
|
56
|
+
out.set(`${loc}:${name}`, p);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** True when a parameter object is marked `required: true`. */
|
|
61
|
+
function isRequiredParam(p) {
|
|
62
|
+
return p.required === true;
|
|
63
|
+
}
|
|
64
|
+
/** True when an operation's request body is marked `required: true`. */
|
|
65
|
+
function requestBodyRequired(op) {
|
|
66
|
+
return isObject(op.requestBody) && op.requestBody.required === true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Compare a baseline OpenAPI document against a current one and classify the
|
|
70
|
+
* differences. The comparison is intentionally conservative: anything that
|
|
71
|
+
* could cause a request that succeeded against the baseline to fail against
|
|
72
|
+
* the current spec is reported as **breaking**; additive and metadata-only
|
|
73
|
+
* changes are reported as **non-breaking**.
|
|
74
|
+
*
|
|
75
|
+
* Detected breaking changes:
|
|
76
|
+
* - a path or operation (HTTP method) present in the baseline is removed;
|
|
77
|
+
* - a documented response status code is removed from an operation;
|
|
78
|
+
* - a new `required` parameter is added to an existing operation;
|
|
79
|
+
* - an existing optional parameter becomes `required`;
|
|
80
|
+
* - an operation's request body becomes required when it was not.
|
|
81
|
+
*
|
|
82
|
+
* Detected non-breaking changes:
|
|
83
|
+
* - new paths, operations, response codes, or optional parameters;
|
|
84
|
+
* - a parameter is removed (the server no longer reads it);
|
|
85
|
+
* - an operation becomes `deprecated`;
|
|
86
|
+
* - the document `info.version` changes.
|
|
87
|
+
*
|
|
88
|
+
* @param baseline - The previously published OpenAPI document (JSON).
|
|
89
|
+
* @param current - The freshly generated OpenAPI document (JSON).
|
|
90
|
+
* @returns Structured lists of breaking and non-breaking changes.
|
|
91
|
+
* @since 0.37.0
|
|
92
|
+
*/
|
|
93
|
+
export function diffOpenAPI(baseline, current) {
|
|
94
|
+
const breaking = [];
|
|
95
|
+
const nonBreaking = [];
|
|
96
|
+
const basePaths = pathsOf(baseline);
|
|
97
|
+
const curPaths = pathsOf(current);
|
|
98
|
+
// info.version change (informational).
|
|
99
|
+
const baseVersion = isObject(baseline) && isObject(baseline.info) ? baseline.info.version : undefined;
|
|
100
|
+
const curVersion = isObject(current) && isObject(current.info) ? current.info.version : undefined;
|
|
101
|
+
if (baseVersion !== curVersion) {
|
|
102
|
+
nonBreaking.push({
|
|
103
|
+
severity: "non-breaking",
|
|
104
|
+
kind: "info.version.changed",
|
|
105
|
+
location: "info.version",
|
|
106
|
+
detail: `version changed from ${JSON.stringify(baseVersion)} to ${JSON.stringify(curVersion)}`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
for (const [path, baseItem] of Object.entries(basePaths)) {
|
|
110
|
+
const curItem = curPaths[path];
|
|
111
|
+
const baseOps = operationsOf(baseItem);
|
|
112
|
+
const curOps = operationsOf(curItem);
|
|
113
|
+
for (const [method, baseOp] of baseOps) {
|
|
114
|
+
const where = `${method.toUpperCase()} ${path}`;
|
|
115
|
+
const curOp = curOps.get(method);
|
|
116
|
+
if (!curOp) {
|
|
117
|
+
breaking.push({
|
|
118
|
+
severity: "breaking",
|
|
119
|
+
kind: "operation.removed",
|
|
120
|
+
location: where,
|
|
121
|
+
detail: `operation ${where} was removed`,
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
diffResponses(baseOp, curOp, where, breaking);
|
|
126
|
+
diffParameters(baseOp, curOp, where, breaking, nonBreaking);
|
|
127
|
+
diffRequestBody(baseOp, curOp, where, breaking);
|
|
128
|
+
if (curOp.deprecated === true && baseOp.deprecated !== true) {
|
|
129
|
+
nonBreaking.push({
|
|
130
|
+
severity: "non-breaking",
|
|
131
|
+
kind: "operation.deprecated",
|
|
132
|
+
location: where,
|
|
133
|
+
detail: `operation ${where} is now deprecated`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Operations added to an existing path.
|
|
138
|
+
for (const [method] of curOps) {
|
|
139
|
+
if (!baseOps.has(method)) {
|
|
140
|
+
nonBreaking.push({
|
|
141
|
+
severity: "non-breaking",
|
|
142
|
+
kind: "operation.added",
|
|
143
|
+
location: `${method.toUpperCase()} ${path}`,
|
|
144
|
+
detail: `operation ${method.toUpperCase()} ${path} was added`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Entirely new paths.
|
|
150
|
+
for (const [path, curItem] of Object.entries(curPaths)) {
|
|
151
|
+
if (path in basePaths)
|
|
152
|
+
continue;
|
|
153
|
+
for (const [method] of operationsOf(curItem)) {
|
|
154
|
+
nonBreaking.push({
|
|
155
|
+
severity: "non-breaking",
|
|
156
|
+
kind: "operation.added",
|
|
157
|
+
location: `${method.toUpperCase()} ${path}`,
|
|
158
|
+
detail: `operation ${method.toUpperCase()} ${path} was added`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return { breaking, nonBreaking };
|
|
163
|
+
}
|
|
164
|
+
/** Report response status codes that existed in the baseline but were removed. */
|
|
165
|
+
function diffResponses(baseOp, curOp, where, breaking) {
|
|
166
|
+
const baseResponses = isObject(baseOp.responses) ? baseOp.responses : {};
|
|
167
|
+
const curResponses = isObject(curOp.responses) ? curOp.responses : {};
|
|
168
|
+
for (const status of Object.keys(baseResponses)) {
|
|
169
|
+
if (!(status in curResponses)) {
|
|
170
|
+
breaking.push({
|
|
171
|
+
severity: "breaking",
|
|
172
|
+
kind: "response.removed",
|
|
173
|
+
location: `${where} → ${status}`,
|
|
174
|
+
detail: `response ${status} was removed from ${where}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** Report parameter additions, removals, and requirement tightening. */
|
|
180
|
+
function diffParameters(baseOp, curOp, where, breaking, nonBreaking) {
|
|
181
|
+
const baseParams = parametersOf(baseOp);
|
|
182
|
+
const curParams = parametersOf(curOp);
|
|
183
|
+
for (const [key, baseParam] of baseParams) {
|
|
184
|
+
const curParam = curParams.get(key);
|
|
185
|
+
if (!curParam) {
|
|
186
|
+
nonBreaking.push({
|
|
187
|
+
severity: "non-breaking",
|
|
188
|
+
kind: "parameter.removed",
|
|
189
|
+
location: `${where} (${key})`,
|
|
190
|
+
detail: `parameter ${key} was removed from ${where}`,
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!isRequiredParam(baseParam) && isRequiredParam(curParam)) {
|
|
195
|
+
breaking.push({
|
|
196
|
+
severity: "breaking",
|
|
197
|
+
kind: "parameter.required.tightened",
|
|
198
|
+
location: `${where} (${key})`,
|
|
199
|
+
detail: `parameter ${key} on ${where} is now required`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const [key, curParam] of curParams) {
|
|
204
|
+
if (baseParams.has(key))
|
|
205
|
+
continue;
|
|
206
|
+
if (isRequiredParam(curParam)) {
|
|
207
|
+
breaking.push({
|
|
208
|
+
severity: "breaking",
|
|
209
|
+
kind: "parameter.required.added",
|
|
210
|
+
location: `${where} (${key})`,
|
|
211
|
+
detail: `new required parameter ${key} was added to ${where}`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
nonBreaking.push({
|
|
216
|
+
severity: "non-breaking",
|
|
217
|
+
kind: "parameter.added",
|
|
218
|
+
location: `${where} (${key})`,
|
|
219
|
+
detail: `new optional parameter ${key} was added to ${where}`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** Report a request body that became required when it previously was not. */
|
|
225
|
+
function diffRequestBody(baseOp, curOp, where, breaking) {
|
|
226
|
+
if (!requestBodyRequired(baseOp) && requestBodyRequired(curOp)) {
|
|
227
|
+
breaking.push({
|
|
228
|
+
severity: "breaking",
|
|
229
|
+
kind: "requestBody.required.added",
|
|
230
|
+
location: where,
|
|
231
|
+
detail: `request body on ${where} is now required`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Convenience predicate over {@link diffOpenAPI}: `true` when the current
|
|
237
|
+
* document introduces at least one breaking change versus the baseline.
|
|
238
|
+
*
|
|
239
|
+
* @param baseline - The previously published OpenAPI document (JSON).
|
|
240
|
+
* @param current - The freshly generated OpenAPI document (JSON).
|
|
241
|
+
* @returns `true` if any breaking change was detected.
|
|
242
|
+
* @since 0.37.0
|
|
243
|
+
*/
|
|
244
|
+
export function hasBreakingChanges(baseline, current) {
|
|
245
|
+
return diffOpenAPI(baseline, current).breaking.length > 0;
|
|
246
|
+
}
|
package/dist/openapi.js
CHANGED
|
@@ -107,7 +107,10 @@ function buildOperation(route, path) {
|
|
|
107
107
|
? { description: route.description ?? meta?.description }
|
|
108
108
|
: {}),
|
|
109
109
|
...(mergedTags.length ? { tags: mergedTags } : {}),
|
|
110
|
-
...(route.deprecated ? { deprecated: true } : {}),
|
|
110
|
+
...(route.deprecated || route.sunset ? { deprecated: true } : {}),
|
|
111
|
+
...(route.sunset
|
|
112
|
+
? { "x-sunset": route.sunset }
|
|
113
|
+
: {}),
|
|
111
114
|
};
|
|
112
115
|
const parameters = [];
|
|
113
116
|
if (path) {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor pagination helpers for DaloyJS.
|
|
3
|
+
*
|
|
4
|
+
* Contract-first list endpoints need three things the framework did not yet
|
|
5
|
+
* ship: an **opaque cursor** the client can echo back without depending on its
|
|
6
|
+
* internals, an **RFC 8288 `Link` header** advertising the `next` / `prev` /
|
|
7
|
+
* `first` pages, and **OpenAPI parameter wiring** so the `cursor` / `limit`
|
|
8
|
+
* query parameters appear in the generated spec and typed client. This module
|
|
9
|
+
* provides all three with zero runtime dependencies:
|
|
10
|
+
*
|
|
11
|
+
* - {@link encodeCursor} / {@link decodeCursor} — base64url-encode an arbitrary
|
|
12
|
+
* JSON-serializable payload (typically the sort key of the last row) into an
|
|
13
|
+
* opaque, URL-safe token, and decode it back with prototype-pollution-safe
|
|
14
|
+
* parsing and a hard size cap.
|
|
15
|
+
* - {@link buildLinkHeader} / {@link buildPageLinks} — assemble a Web-standard
|
|
16
|
+
* `Link` header, with CRLF / angle-bracket header-injection guards baked in.
|
|
17
|
+
* - {@link paginationQuery} — a Standard Schema validator for the `cursor` +
|
|
18
|
+
* `limit` query parameters that both validates at runtime (clamping `limit`
|
|
19
|
+
* to a safe range) **and** advertises itself to the OpenAPI generator via a
|
|
20
|
+
* `toJSONSchema()` method, so `request: { query: paginationQuery() }` wires
|
|
21
|
+
* the parameters into the contract with no extra code.
|
|
22
|
+
*
|
|
23
|
+
* Everything here is built on Web-standard `URL` / `Request` and `btoa` /
|
|
24
|
+
* `atob`, so it runs unchanged on Node, Bun, Deno, Cloudflare Workers, and
|
|
25
|
+
* Vercel Edge.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
* @since 0.37.0
|
|
29
|
+
*/
|
|
30
|
+
import type { StandardSchemaV1 } from "./schema.js";
|
|
31
|
+
/**
|
|
32
|
+
* Hard cap on the length of an encoded cursor string accepted by
|
|
33
|
+
* {@link decodeCursor}. Bounds the work an attacker can force by sending a
|
|
34
|
+
* giant `cursor` query parameter. 4 KiB is far larger than any legitimate
|
|
35
|
+
* sort-key payload.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_CURSOR_LENGTH = 4096;
|
|
38
|
+
/**
|
|
39
|
+
* Encode an arbitrary JSON-serializable value into an opaque, URL-safe cursor
|
|
40
|
+
* token (base64url, no padding).
|
|
41
|
+
*
|
|
42
|
+
* The token is **opaque, not secret**: it is encoded, not encrypted or signed.
|
|
43
|
+
* Never trust a decoded cursor for authorization — always re-scope the
|
|
44
|
+
* underlying query by the authenticated principal on the server. Put only the
|
|
45
|
+
* data you need to resume a scan (e.g. `{ id, createdAt }`) inside it.
|
|
46
|
+
*
|
|
47
|
+
* @param payload - Any JSON-serializable value (object, array, string, …).
|
|
48
|
+
* @returns A base64url cursor string safe to place in a URL or `Link` header.
|
|
49
|
+
* @throws {TypeError} If `payload` cannot be JSON-serialized (e.g. a `BigInt`
|
|
50
|
+
* or a circular structure).
|
|
51
|
+
* @since 0.37.0
|
|
52
|
+
*/
|
|
53
|
+
export declare function encodeCursor(payload: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* Decode an opaque cursor produced by {@link encodeCursor} back into its
|
|
56
|
+
* original value.
|
|
57
|
+
*
|
|
58
|
+
* Parsing is hardened: the input length is capped at {@link MAX_CURSOR_LENGTH},
|
|
59
|
+
* decoding rejects malformed base64url, and any `__proto__` / `constructor` /
|
|
60
|
+
* `prototype` keys in the decoded object graph are stripped (prototype-
|
|
61
|
+
* pollution defense, mirroring the core body parsers).
|
|
62
|
+
*
|
|
63
|
+
* @typeParam T - The expected shape of the decoded payload (caller-asserted).
|
|
64
|
+
* @param cursor - The opaque cursor string from the request.
|
|
65
|
+
* @returns The decoded payload.
|
|
66
|
+
* @throws {BadRequestError} If the cursor is missing, over-long, or malformed —
|
|
67
|
+
* a `400` so a tampered cursor surfaces as a client error, not a `500`.
|
|
68
|
+
* @since 0.37.0
|
|
69
|
+
*/
|
|
70
|
+
export declare function decodeCursor<T = unknown>(cursor: string): T;
|
|
71
|
+
/** A single web link for an RFC 8288 `Link` header. */
|
|
72
|
+
export interface PaginationLink {
|
|
73
|
+
/** Target URI-Reference (the `<...>` portion). */
|
|
74
|
+
url: string;
|
|
75
|
+
/** Relation type, e.g. `"next"`, `"prev"`, `"first"`, `"last"`. */
|
|
76
|
+
rel: string;
|
|
77
|
+
/** Optional human-readable `title` parameter. */
|
|
78
|
+
title?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Serialize a list of links into a single RFC 8288 `Link` header value.
|
|
82
|
+
*
|
|
83
|
+
* Each entry renders as `<url>; rel="rel"` (plus `; title="…"` when present).
|
|
84
|
+
* URLs containing control characters, `<`, or `>` and rel/title values
|
|
85
|
+
* containing control characters, `"`, or `\` are rejected — a structural
|
|
86
|
+
* defense against `Link`-header / response-splitting injection.
|
|
87
|
+
*
|
|
88
|
+
* @param links - The links to emit. An empty array yields an empty string.
|
|
89
|
+
* @returns The comma-joined `Link` header value.
|
|
90
|
+
* @throws {Error} If any URL or token contains forbidden characters.
|
|
91
|
+
* @since 0.37.0
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildLinkHeader(links: readonly PaginationLink[]): string;
|
|
94
|
+
/** Options for {@link buildPageLinks}. */
|
|
95
|
+
export interface PageLinkOptions {
|
|
96
|
+
/** The current request URL (string or `URL`); other query params are kept. */
|
|
97
|
+
url: string | URL;
|
|
98
|
+
/** Query-parameter name carrying the cursor. Default: `"cursor"`. */
|
|
99
|
+
cursorParam?: string;
|
|
100
|
+
/** Opaque cursor for the next page, or `null`/`undefined` to omit `next`. */
|
|
101
|
+
next?: string | null;
|
|
102
|
+
/** Opaque cursor for the previous page, or `null`/`undefined` to omit `prev`. */
|
|
103
|
+
prev?: string | null;
|
|
104
|
+
/**
|
|
105
|
+
* Emit a `rel="first"` link (the current URL with the cursor param removed).
|
|
106
|
+
* Default: `false`.
|
|
107
|
+
*/
|
|
108
|
+
first?: boolean;
|
|
109
|
+
/** Extra links appended verbatim (e.g. a `rel="last"`). */
|
|
110
|
+
extraLinks?: readonly PaginationLink[];
|
|
111
|
+
}
|
|
112
|
+
/** Result of {@link buildPageLinks}. */
|
|
113
|
+
export interface PageLinks {
|
|
114
|
+
/** The structured links, ready for {@link buildLinkHeader} or a JSON body. */
|
|
115
|
+
links: PaginationLink[];
|
|
116
|
+
/** The serialized RFC 8288 `Link` header value (empty when no links). */
|
|
117
|
+
linkHeader: string;
|
|
118
|
+
/** Convenience map of the computed page URLs. */
|
|
119
|
+
urls: {
|
|
120
|
+
self: string;
|
|
121
|
+
next?: string;
|
|
122
|
+
prev?: string;
|
|
123
|
+
first?: string;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the `next` / `prev` / `first` page URLs for a list response by cloning
|
|
128
|
+
* the current request URL and swapping its cursor query parameter, then
|
|
129
|
+
* serialize them into an RFC 8288 `Link` header.
|
|
130
|
+
*
|
|
131
|
+
* All other query parameters (filters, `limit`, …) are preserved, so the
|
|
132
|
+
* generated links are drop-in "give me the same query, next page" URLs.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const { linkHeader } = buildPageLinks({
|
|
137
|
+
* url: ctx.request.url,
|
|
138
|
+
* next: nextCursor, // from encodeCursor(...)
|
|
139
|
+
* prev: prevCursor,
|
|
140
|
+
* first: true,
|
|
141
|
+
* });
|
|
142
|
+
* set.headers.set("Link", linkHeader);
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @param opts - Current URL plus the cursors to advertise.
|
|
146
|
+
* @returns The structured links, the `Link` header string, and the page URLs.
|
|
147
|
+
* @since 0.37.0
|
|
148
|
+
*/
|
|
149
|
+
export declare function buildPageLinks(opts: PageLinkOptions): PageLinks;
|
|
150
|
+
/** Options for {@link paginationQuery}. */
|
|
151
|
+
export interface PaginationQueryOptions {
|
|
152
|
+
/** Query-parameter name for the cursor. Default: `"cursor"`. */
|
|
153
|
+
cursorParam?: string;
|
|
154
|
+
/** Query-parameter name for the page size. Default: `"limit"`. */
|
|
155
|
+
limitParam?: string;
|
|
156
|
+
/** Default page size applied when `limit` is omitted. Default: `min(20, maxLimit)`. */
|
|
157
|
+
defaultLimit?: number;
|
|
158
|
+
/** Minimum accepted page size. Default: `1`. */
|
|
159
|
+
minLimit?: number;
|
|
160
|
+
/** Maximum accepted page size (also caps over-large requests). Default: `100`. */
|
|
161
|
+
maxLimit?: number;
|
|
162
|
+
}
|
|
163
|
+
/** Validated output of {@link paginationQuery}. */
|
|
164
|
+
export interface PaginationParams {
|
|
165
|
+
/** The resolved page size, clamped to `[minLimit, maxLimit]`. */
|
|
166
|
+
limit: number;
|
|
167
|
+
/** The opaque cursor, if the client supplied one. */
|
|
168
|
+
cursor?: string;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* A Standard Schema for cursor-pagination query parameters that also carries a
|
|
172
|
+
* `toJSONSchema()` method so the OpenAPI generator wires the `cursor` and
|
|
173
|
+
* `limit` parameters into the contract automatically.
|
|
174
|
+
*/
|
|
175
|
+
export interface PaginationQuerySchema extends StandardSchemaV1<Record<string, unknown>, PaginationParams> {
|
|
176
|
+
/** Used by the OpenAPI generator to emit the query parameters. */
|
|
177
|
+
toJSONSchema(): Record<string, unknown>;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Build a Standard Schema validator for cursor-pagination query parameters.
|
|
181
|
+
*
|
|
182
|
+
* Use it as a route's `request.query`. At runtime it parses and validates
|
|
183
|
+
* `limit` (coerced from its string query value to an integer and clamped to
|
|
184
|
+
* `[minLimit, maxLimit]`, defaulting to `defaultLimit` when absent) and passes
|
|
185
|
+
* `cursor` through as an optional opaque string. Because it also exposes
|
|
186
|
+
* `toJSONSchema()`, the same call wires both parameters into the generated
|
|
187
|
+
* OpenAPI document and typed client — no duplicate parameter declarations.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* app.route({
|
|
192
|
+
* method: "GET",
|
|
193
|
+
* path: "/books",
|
|
194
|
+
* operationId: "listBooks",
|
|
195
|
+
* request: { query: paginationQuery({ defaultLimit: 25, maxLimit: 100 }) },
|
|
196
|
+
* responses: { 200: { description: "ok", body: pageSchema } },
|
|
197
|
+
* handler: async ({ query }) => {
|
|
198
|
+
* const { limit, cursor } = query; // fully typed + validated
|
|
199
|
+
* // ...
|
|
200
|
+
* },
|
|
201
|
+
* });
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @param opts - Parameter names and page-size bounds.
|
|
205
|
+
* @returns A Standard Schema usable as `request.query`.
|
|
206
|
+
* @throws {Error} If the configured bounds are not positive integers or are
|
|
207
|
+
* inconsistent (`minLimit > maxLimit`, `defaultLimit` out of range).
|
|
208
|
+
* @since 0.37.0
|
|
209
|
+
*/
|
|
210
|
+
export declare function paginationQuery(opts?: PaginationQueryOptions): PaginationQuerySchema;
|