@maschinenlesbar.org/pegel-online-cli 0.1.0 → 0.3.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 +29 -8
- package/dist/src/cli/commands/stations.d.ts +0 -1
- package/dist/src/cli/commands/stations.js +29 -5
- package/dist/src/cli/commands/timeseries.d.ts +0 -1
- package/dist/src/cli/commands/timeseries.js +3 -4
- package/dist/src/cli/index.d.ts +0 -1
- package/dist/src/cli/index.js +0 -1
- package/dist/src/cli/io.d.ts +2 -2
- package/dist/src/cli/io.js +7 -3
- package/dist/src/cli/program.d.ts +0 -1
- package/dist/src/cli/program.js +6 -7
- package/dist/src/cli/run.d.ts +12 -1
- package/dist/src/cli/run.js +35 -2
- package/dist/src/cli/shared.d.ts +21 -15
- package/dist/src/cli/shared.js +39 -48
- package/dist/src/client/client.d.ts +46 -1
- package/dist/src/client/client.js +152 -27
- package/dist/src/client/engine.d.ts +51 -13
- package/dist/src/client/engine.js +404 -81
- package/dist/src/client/errors.d.ts +38 -2
- package/dist/src/client/errors.js +74 -4
- package/dist/src/client/http.d.ts +26 -1
- package/dist/src/client/http.js +16 -2
- package/dist/src/client/index.d.ts +6 -4
- package/dist/src/client/index.js +4 -4
- package/dist/src/client/query.d.ts +0 -1
- package/dist/src/client/query.js +0 -1
- package/dist/src/client/types.d.ts +60 -10
- package/dist/src/client/types.js +0 -1
- package/dist/src/client/validate.d.ts +72 -0
- package/dist/src/client/validate.js +155 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/package.json +4 -3
- package/dist/src/cli/commands/stations.d.ts.map +0 -1
- package/dist/src/cli/commands/stations.js.map +0 -1
- package/dist/src/cli/commands/timeseries.d.ts.map +0 -1
- package/dist/src/cli/commands/timeseries.js.map +0 -1
- package/dist/src/cli/index.d.ts.map +0 -1
- package/dist/src/cli/index.js.map +0 -1
- package/dist/src/cli/io.d.ts.map +0 -1
- package/dist/src/cli/io.js.map +0 -1
- package/dist/src/cli/program.d.ts.map +0 -1
- package/dist/src/cli/program.js.map +0 -1
- package/dist/src/cli/run.d.ts.map +0 -1
- package/dist/src/cli/run.js.map +0 -1
- package/dist/src/cli/shared.d.ts.map +0 -1
- package/dist/src/cli/shared.js.map +0 -1
- package/dist/src/client/client.d.ts.map +0 -1
- package/dist/src/client/client.js.map +0 -1
- package/dist/src/client/engine.d.ts.map +0 -1
- package/dist/src/client/engine.js.map +0 -1
- package/dist/src/client/errors.d.ts.map +0 -1
- package/dist/src/client/errors.js.map +0 -1
- package/dist/src/client/http.d.ts.map +0 -1
- package/dist/src/client/http.js.map +0 -1
- package/dist/src/client/index.d.ts.map +0 -1
- package/dist/src/client/index.js.map +0 -1
- package/dist/src/client/query.d.ts.map +0 -1
- package/dist/src/client/query.js.map +0 -1
- package/dist/src/client/types.d.ts.map +0 -1
- package/dist/src/client/types.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
|
@@ -7,9 +7,29 @@ export declare class PegelError extends Error {
|
|
|
7
7
|
/**
|
|
8
8
|
* Replace the userinfo of a URL (`https://user:secret@host/...`) with `***`, so a
|
|
9
9
|
* credential in a base URL never reaches an error message, a log or CI output.
|
|
10
|
-
* A
|
|
10
|
+
* A value that does not parse as a URL (a port typo, an unencoded "#" in the
|
|
11
|
+
* password) or has no scheme (`user:pw@host`) is cut by text instead
|
|
12
|
+
* (`credentialsIn` + `redactCredentials`); one without userinfo is returned unchanged.
|
|
11
13
|
*/
|
|
12
14
|
export declare function redactUrl(url: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* The userinfo a URL-like value carries, exactly as written — `["alice:pa#ss"]` for
|
|
17
|
+
* `https://alice:pa#ss@host` — or `[]` when it carries none. It works on values that
|
|
18
|
+
* don't parse as a URL too, and on values with a prefix (`--base-url=https://u:p@h`):
|
|
19
|
+
* the userinfo is everything between `://` and the last `@` before the host. A value
|
|
20
|
+
* without a scheme counts when it reads `user:password@host`. Used to redact those
|
|
21
|
+
* exact strings from text that echoes the value (usage errors, help), whatever
|
|
22
|
+
* characters the password contains.
|
|
23
|
+
*/
|
|
24
|
+
export declare function credentialsIn(value: string): string[];
|
|
25
|
+
/**
|
|
26
|
+
* `text` with every occurrence of each credential (as `credentialsIn` returns them)
|
|
27
|
+
* that is followed by `@` replaced by `***`. Matching the exact strings, not a
|
|
28
|
+
* pattern, covers passwords with spaces, quotes, `#`, `?` or `/` that no URL pattern
|
|
29
|
+
* can delimit. The percent-encoded form (`alice%3Apw%40`, as a URL typed where a
|
|
30
|
+
* station id belongs ends up in a request path) is replaced too.
|
|
31
|
+
*/
|
|
32
|
+
export declare function redactCredentials(text: string, credentials: readonly string[]): string;
|
|
13
33
|
/**
|
|
14
34
|
* The API responded with a non-2xx status code. `detail` holds a human-readable
|
|
15
35
|
* message extracted from the response body when one is present.
|
|
@@ -26,6 +46,12 @@ export declare class PegelApiError extends PegelError {
|
|
|
26
46
|
* redacted. The message names it.
|
|
27
47
|
*/
|
|
28
48
|
readonly location: string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* For a 429/503 that was not retried because its `Retry-After` asked for longer than
|
|
51
|
+
* the client waits (`MAX_RETRY_AFTER_MS`, 30 s): the wait the server asked for, in
|
|
52
|
+
* milliseconds. Retrying before then won't help.
|
|
53
|
+
*/
|
|
54
|
+
readonly retryAfterMs: number | undefined;
|
|
29
55
|
constructor(args: {
|
|
30
56
|
status: number;
|
|
31
57
|
url: string;
|
|
@@ -35,14 +61,24 @@ export declare class PegelApiError extends PegelError {
|
|
|
35
61
|
location?: string;
|
|
36
62
|
/** Redirects already followed when the limit stopped this one (> 0 only). */
|
|
37
63
|
redirectsFollowed?: number;
|
|
64
|
+
/** A Retry-After longer than the client waits (not retried). */
|
|
65
|
+
retryAfterMs?: number;
|
|
66
|
+
/** Advice appended to the message (e.g. that a redirect dropped the credentials). */
|
|
67
|
+
hint?: string;
|
|
38
68
|
});
|
|
39
69
|
/** True for statuses the API documents as transient and retry-able. */
|
|
40
70
|
get isRetryable(): boolean;
|
|
41
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* An input the library rejects before sending any request: a client option or a
|
|
74
|
+
* method argument that breaks one of the rules in `validate.ts`. The message reads
|
|
75
|
+
* `Invalid <name>: <reason>`. The CLI reports it as a usage error (exit 2).
|
|
76
|
+
*/
|
|
77
|
+
export declare class PegelValidationError extends PegelError {
|
|
78
|
+
}
|
|
42
79
|
/** A transport-level failure (DNS, connection reset, timeout, ...). */
|
|
43
80
|
export declare class PegelNetworkError extends PegelError {
|
|
44
81
|
}
|
|
45
82
|
/** The response body could not be parsed as the expected JSON shape. */
|
|
46
83
|
export declare class PegelParseError extends PegelError {
|
|
47
84
|
}
|
|
48
|
-
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -10,7 +10,9 @@ export class PegelError extends Error {
|
|
|
10
10
|
/**
|
|
11
11
|
* Replace the userinfo of a URL (`https://user:secret@host/...`) with `***`, so a
|
|
12
12
|
* credential in a base URL never reaches an error message, a log or CI output.
|
|
13
|
-
* A
|
|
13
|
+
* A value that does not parse as a URL (a port typo, an unencoded "#" in the
|
|
14
|
+
* password) or has no scheme (`user:pw@host`) is cut by text instead
|
|
15
|
+
* (`credentialsIn` + `redactCredentials`); one without userinfo is returned unchanged.
|
|
14
16
|
*/
|
|
15
17
|
export function redactUrl(url) {
|
|
16
18
|
let parsed;
|
|
@@ -18,14 +20,63 @@ export function redactUrl(url) {
|
|
|
18
20
|
parsed = new URL(url);
|
|
19
21
|
}
|
|
20
22
|
catch {
|
|
21
|
-
return url;
|
|
23
|
+
return redactCredentials(url, credentialsIn(url));
|
|
22
24
|
}
|
|
25
|
+
// `user:pw@host` without a scheme parses as a URL with the scheme "user:": no userinfo.
|
|
23
26
|
if (parsed.username === "" && parsed.password === "")
|
|
24
|
-
return url;
|
|
27
|
+
return redactCredentials(url, credentialsIn(url));
|
|
25
28
|
parsed.username = "***";
|
|
26
29
|
parsed.password = "";
|
|
27
30
|
return parsed.href;
|
|
28
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The userinfo a URL-like value carries, exactly as written — `["alice:pa#ss"]` for
|
|
34
|
+
* `https://alice:pa#ss@host` — or `[]` when it carries none. It works on values that
|
|
35
|
+
* don't parse as a URL too, and on values with a prefix (`--base-url=https://u:p@h`):
|
|
36
|
+
* the userinfo is everything between `://` and the last `@` before the host. A value
|
|
37
|
+
* without a scheme counts when it reads `user:password@host`. Used to redact those
|
|
38
|
+
* exact strings from text that echoes the value (usage errors, help), whatever
|
|
39
|
+
* characters the password contains.
|
|
40
|
+
*/
|
|
41
|
+
export function credentialsIn(value) {
|
|
42
|
+
const schemeAt = value.indexOf("://");
|
|
43
|
+
const rest = schemeAt >= 0 ? value.slice(schemeAt + 3) : value;
|
|
44
|
+
// Without a scheme only the unmistakable `user:password@host` form counts.
|
|
45
|
+
if (schemeAt < 0 && !/^[^\s/@:]+:[^@]*@[^@\s/]/.test(rest))
|
|
46
|
+
return [];
|
|
47
|
+
// The URL itself starts at its scheme (`--base-url=https://…` has a prefix).
|
|
48
|
+
const scheme = schemeAt >= 0 ? /[a-z][a-z0-9+.-]*$/i.exec(value.slice(0, schemeAt)) : null;
|
|
49
|
+
let parses = false;
|
|
50
|
+
try {
|
|
51
|
+
new URL(schemeAt >= 0 ? value.slice(scheme?.index ?? schemeAt) : `http://${rest}`);
|
|
52
|
+
parses = true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Doesn't parse: the password may hold "/", "?", "#" or spaces.
|
|
56
|
+
}
|
|
57
|
+
// In a URL that parses, the userinfo ends at the last "@" of the authority (before
|
|
58
|
+
// the first "/", "?" or "#"); in one that doesn't, at the last "@" of the value.
|
|
59
|
+
const authority = parses ? rest.slice(0, rest.search(/[/?#]|$/)) : rest;
|
|
60
|
+
const end = authority.lastIndexOf("@");
|
|
61
|
+
return end > 0 ? [rest.slice(0, end)] : [];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* `text` with every occurrence of each credential (as `credentialsIn` returns them)
|
|
65
|
+
* that is followed by `@` replaced by `***`. Matching the exact strings, not a
|
|
66
|
+
* pattern, covers passwords with spaces, quotes, `#`, `?` or `/` that no URL pattern
|
|
67
|
+
* can delimit. The percent-encoded form (`alice%3Apw%40`, as a URL typed where a
|
|
68
|
+
* station id belongs ends up in a request path) is replaced too.
|
|
69
|
+
*/
|
|
70
|
+
export function redactCredentials(text, credentials) {
|
|
71
|
+
let out = text;
|
|
72
|
+
for (const secret of credentials) {
|
|
73
|
+
if (secret === "")
|
|
74
|
+
continue;
|
|
75
|
+
out = out.split(`${secret}@`).join("***@");
|
|
76
|
+
out = out.split(`${encodeURIComponent(secret)}%40`).join("***%40");
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
29
80
|
/**
|
|
30
81
|
* The API responded with a non-2xx status code. `detail` holds a human-readable
|
|
31
82
|
* message extracted from the response body when one is present.
|
|
@@ -42,6 +93,12 @@ export class PegelApiError extends PegelError {
|
|
|
42
93
|
* redacted. The message names it.
|
|
43
94
|
*/
|
|
44
95
|
location;
|
|
96
|
+
/**
|
|
97
|
+
* For a 429/503 that was not retried because its `Retry-After` asked for longer than
|
|
98
|
+
* the client waits (`MAX_RETRY_AFTER_MS`, 30 s): the wait the server asked for, in
|
|
99
|
+
* milliseconds. Retrying before then won't help.
|
|
100
|
+
*/
|
|
101
|
+
retryAfterMs;
|
|
45
102
|
constructor(args) {
|
|
46
103
|
const parts = [];
|
|
47
104
|
if (args.detail)
|
|
@@ -54,6 +111,12 @@ export class PegelApiError extends PegelError {
|
|
|
54
111
|
? `redirect to ${args.location} not followed${limit}`
|
|
55
112
|
: "redirect not followed (no Location header)");
|
|
56
113
|
}
|
|
114
|
+
if (args.retryAfterMs !== undefined) {
|
|
115
|
+
parts.push(`the server asked to wait ${Math.ceil(args.retryAfterMs / 1000)} s (Retry-After), longer than the 30 s ` +
|
|
116
|
+
"the client waits, so it was not retried; try again after that");
|
|
117
|
+
}
|
|
118
|
+
if (args.hint)
|
|
119
|
+
parts.push(args.hint);
|
|
57
120
|
const detailPart = parts.length > 0 ? `: ${parts.join("; ")}` : "";
|
|
58
121
|
// The URL is shown without userinfo: a credential in --base-url must not leak.
|
|
59
122
|
const url = redactUrl(args.url);
|
|
@@ -64,16 +127,23 @@ export class PegelApiError extends PegelError {
|
|
|
64
127
|
this.body = args.body;
|
|
65
128
|
this.detail = args.detail;
|
|
66
129
|
this.location = args.location;
|
|
130
|
+
this.retryAfterMs = args.retryAfterMs;
|
|
67
131
|
}
|
|
68
132
|
/** True for statuses the API documents as transient and retry-able. */
|
|
69
133
|
get isRetryable() {
|
|
70
134
|
return this.status === 429 || this.status === 503;
|
|
71
135
|
}
|
|
72
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* An input the library rejects before sending any request: a client option or a
|
|
139
|
+
* method argument that breaks one of the rules in `validate.ts`. The message reads
|
|
140
|
+
* `Invalid <name>: <reason>`. The CLI reports it as a usage error (exit 2).
|
|
141
|
+
*/
|
|
142
|
+
export class PegelValidationError extends PegelError {
|
|
143
|
+
}
|
|
73
144
|
/** A transport-level failure (DNS, connection reset, timeout, ...). */
|
|
74
145
|
export class PegelNetworkError extends PegelError {
|
|
75
146
|
}
|
|
76
147
|
/** The response body could not be parsed as the expected JSON shape. */
|
|
77
148
|
export class PegelParseError extends PegelError {
|
|
78
149
|
}
|
|
79
|
-
//# sourceMappingURL=errors.js.map
|
|
@@ -6,17 +6,43 @@ export interface HttpRequest {
|
|
|
6
6
|
headers?: Record<string, string>;
|
|
7
7
|
/** Optional request body (already serialised). */
|
|
8
8
|
body?: string | Buffer;
|
|
9
|
+
/**
|
|
10
|
+
* Always `"manual"`: a transport must not follow redirects itself (fetch does by
|
|
11
|
+
* default — pass `redirect: request.redirect`). The engine follows them, keeping the
|
|
12
|
+
* base URL's credentials on the same origin and dropping them, and credential headers,
|
|
13
|
+
* on another. A response whose `url` (the final URL a fetch transport may report) lies
|
|
14
|
+
* on another origin than the request is rejected as a PegelNetworkError.
|
|
15
|
+
*/
|
|
16
|
+
redirect?: "manual";
|
|
9
17
|
/** Timeout for the whole request, response body included, in milliseconds. */
|
|
10
18
|
timeoutMs?: number;
|
|
11
19
|
/** Hard cap on the response body size in bytes; the request aborts if exceeded. */
|
|
12
20
|
maxResponseBytes?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Aborted when the engine's overall deadline (`timeoutMs`) passes. A transport should
|
|
23
|
+
* stop the request then (`fetch(url, { signal })`); the engine rejects at the deadline
|
|
24
|
+
* either way, and enforces `maxResponseBytes` on the body it gets back, so neither
|
|
25
|
+
* limit depends on it.
|
|
26
|
+
*/
|
|
27
|
+
signal?: AbortSignal;
|
|
13
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* What a transport resolves with. The engine is lenient about the shapes a custom
|
|
31
|
+
* transport naturally returns: `headers` may be a plain record in any letter case, a
|
|
32
|
+
* WHATWG `Headers` object or a `Map`; `body` may be a Buffer, any `ArrayBuffer` view
|
|
33
|
+
* (fetch's `Uint8Array`, from any realm) or an `ArrayBuffer`. Anything else, or a
|
|
34
|
+
* `status` outside 100–599, is a PegelNetworkError.
|
|
35
|
+
*/
|
|
14
36
|
export interface HttpResponse {
|
|
15
37
|
status: number;
|
|
16
38
|
headers: http.IncomingHttpHeaders;
|
|
17
39
|
body: Buffer;
|
|
40
|
+
/** The URL that answered, if the transport knows it (fetch's `Response.url`). */
|
|
41
|
+
url?: string;
|
|
18
42
|
}
|
|
19
43
|
export type Transport = (request: HttpRequest) => Promise<HttpResponse>;
|
|
44
|
+
/** The message for a body over the size cap, naming the option on both sides. */
|
|
45
|
+
export declare function sizeLimitMessage(maxBytes: number): string;
|
|
20
46
|
/**
|
|
21
47
|
* The longest delay Node's timers support (2^31 - 1 ms, about 24.8 days). A longer one
|
|
22
48
|
* prints a TimeoutOverflowWarning and fires after 1 ms, so timeouts are capped here.
|
|
@@ -28,4 +54,3 @@ export declare const MAX_TIMEOUT_MS = 2147483647;
|
|
|
28
54
|
* (connection errors, timeouts, malformed URLs).
|
|
29
55
|
*/
|
|
30
56
|
export declare const nodeHttpTransport: Transport;
|
|
31
|
-
//# sourceMappingURL=http.d.ts.map
|
package/dist/src/client/http.js
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import http from "node:http";
|
|
9
9
|
import https from "node:https";
|
|
10
10
|
import { PegelNetworkError, redactUrl } from "./errors.js";
|
|
11
|
+
/** The message for a body over the size cap, naming the option on both sides. */
|
|
12
|
+
export function sizeLimitMessage(maxBytes) {
|
|
13
|
+
return `Response exceeded the size limit of ${maxBytes} bytes (maxResponseBytes; --max-response-bytes on the CLI)`;
|
|
14
|
+
}
|
|
11
15
|
/**
|
|
12
16
|
* The longest delay Node's timers support (2^31 - 1 ms, about 24.8 days). A longer one
|
|
13
17
|
* prints a TimeoutOverflowWarning and fires after 1 ms, so timeouts are capped here.
|
|
@@ -71,7 +75,7 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
|
|
|
71
75
|
if (maxBytes !== undefined && received > maxBytes) {
|
|
72
76
|
aborted = true;
|
|
73
77
|
res.destroy();
|
|
74
|
-
settleReject(new PegelNetworkError(
|
|
78
|
+
settleReject(new PegelNetworkError(sizeLimitMessage(maxBytes)));
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
81
|
chunks.push(chunk);
|
|
@@ -101,6 +105,17 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
|
|
|
101
105
|
// Do not let the deadline timer alone keep the process alive.
|
|
102
106
|
deadlineTimer.unref?.();
|
|
103
107
|
}
|
|
108
|
+
if (request.signal !== undefined) {
|
|
109
|
+
const abort = () => {
|
|
110
|
+
const err = new PegelNetworkError(`Request timed out after ${request.timeoutMs ?? 0}ms`);
|
|
111
|
+
settleReject(err);
|
|
112
|
+
req.destroy(err);
|
|
113
|
+
};
|
|
114
|
+
if (request.signal.aborted)
|
|
115
|
+
abort();
|
|
116
|
+
else
|
|
117
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
118
|
+
}
|
|
104
119
|
req.on("error", (err) => {
|
|
105
120
|
// A timeout destroy already passes an PegelNetworkError; don't double-wrap.
|
|
106
121
|
settleReject(err instanceof PegelNetworkError ? err : new PegelNetworkError(err.message, { cause: err }));
|
|
@@ -109,4 +124,3 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
|
|
|
109
124
|
req.write(request.body);
|
|
110
125
|
req.end();
|
|
111
126
|
});
|
|
112
|
-
//# sourceMappingURL=http.js.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
export { PegelOnlineClient } from "./client.js";
|
|
2
|
-
export {
|
|
1
|
+
export { NO_VALUE_SENTINEL, PegelOnlineClient, stationListNotes } from "./client.js";
|
|
2
|
+
export type { StationListNote } from "./client.js";
|
|
3
|
+
export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, isTransientNetworkError, parseRetryAfter, validateBaseUrl, } from "./engine.js";
|
|
3
4
|
export type { EngineOptions, RawResponse } from "./engine.js";
|
|
4
5
|
export { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
|
|
5
6
|
export type { Transport, HttpRequest, HttpResponse } from "./http.js";
|
|
6
7
|
export { buildQueryString } from "./query.js";
|
|
7
8
|
export type { QueryParams, QueryValue } from "./query.js";
|
|
8
|
-
export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, redactUrl } from "./errors.js";
|
|
9
|
+
export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, PegelValidationError, redactUrl, credentialsIn, redactCredentials, } from "./errors.js";
|
|
10
|
+
export { assertValid, baseUrlProblem, baseUrlWhitespaceProblem, headerValueProblem, idListProblem, isBlank, knownKeysProblem, nonEmptyProblem, normalizeInput, optionalBooleanProblem, } from "./validate.js";
|
|
11
|
+
export type { Problem } from "./validate.js";
|
|
9
12
|
export * from "./types.js";
|
|
10
|
-
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/client/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Public entry point for the API client library.
|
|
2
|
-
export { PegelOnlineClient } from "./client.js";
|
|
3
|
-
export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, parseRetryAfter, } from "./engine.js";
|
|
2
|
+
export { NO_VALUE_SENTINEL, PegelOnlineClient, stationListNotes } from "./client.js";
|
|
3
|
+
export { RequestEngine, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, isTransientNetworkError, parseRetryAfter, validateBaseUrl, } from "./engine.js";
|
|
4
4
|
export { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
|
|
5
5
|
export { buildQueryString } from "./query.js";
|
|
6
|
-
export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, redactUrl } from "./errors.js";
|
|
6
|
+
export { PegelError, PegelApiError, PegelNetworkError, PegelParseError, PegelValidationError, redactUrl, credentialsIn, redactCredentials, } from "./errors.js";
|
|
7
|
+
export { assertValid, baseUrlProblem, baseUrlWhitespaceProblem, headerValueProblem, idListProblem, isBlank, knownKeysProblem, nonEmptyProblem, normalizeInput, optionalBooleanProblem, } from "./validate.js";
|
|
7
8
|
export * from "./types.js";
|
|
8
|
-
//# sourceMappingURL=index.js.map
|
package/dist/src/client/query.js
CHANGED
|
@@ -24,14 +24,32 @@ export interface Station {
|
|
|
24
24
|
water?: Water;
|
|
25
25
|
timeseries?: TimeseriesInfo[];
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* The API's classification of a current water level (`stateMnwMhw`, `stateNswHsw`), as
|
|
29
|
+
* documented upstream:
|
|
30
|
+
* - `low` — at or below MNW (stateMnwMhw only);
|
|
31
|
+
* - `normal` — between MNW and MHW, or between 0 and HSW;
|
|
32
|
+
* - `high` — at or above MHW, or HSW;
|
|
33
|
+
* - `unknown` — the series has no MNW/MHW (or HSW) mark to compare with;
|
|
34
|
+
* - `commented` — **gauge malfunction or disruption** ("Fehlfunktion oder Störung"): the
|
|
35
|
+
* value may be wrong; the reason is in the series' `comment` (`TimeseriesInfo.comment`);
|
|
36
|
+
* - `out-dated` — the reading is older than 25 hours.
|
|
37
|
+
* Typed open (`string & {}`) so a value the API adds later still type-checks.
|
|
38
|
+
*/
|
|
39
|
+
export type MeasurementState = "low" | "normal" | "high" | "unknown" | "commented" | "out-dated" | (string & {});
|
|
27
40
|
/** A measurement value plus the API's state classifications. */
|
|
28
41
|
export interface CurrentMeasurement {
|
|
29
42
|
timestamp: string;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The reading, in the unit of its timeseries (`TimeseriesInfo.unit` — not always `cm`
|
|
45
|
+
* for `W`). `null` when the gauge reported no value: the client maps the sentinel
|
|
46
|
+
* `99999` (`NO_VALUE_SENTINEL`) the API relays for that to `null`.
|
|
47
|
+
*/
|
|
48
|
+
value: number | null;
|
|
49
|
+
/** Classification vs. the mean low/high water marks (water levels only). */
|
|
50
|
+
stateMnwMhw?: MeasurementState;
|
|
51
|
+
/** Classification vs. the lowest/highest navigable water marks (water levels only). */
|
|
52
|
+
stateNswHsw?: MeasurementState;
|
|
35
53
|
}
|
|
36
54
|
/** The datum a water-level series is measured from (Pegelnullpunkt). */
|
|
37
55
|
export interface GaugeZero {
|
|
@@ -41,14 +59,37 @@ export interface GaugeZero {
|
|
|
41
59
|
/** Date the datum applies from, e.g. "2019-11-01". */
|
|
42
60
|
validFrom?: string;
|
|
43
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* An operator's note on a timeseries, present while something is wrong with it — e.g.
|
|
64
|
+
* "Funktionsstörung, fehlerhafte Messwerte" (malfunction, faulty readings) at RINTELN,
|
|
65
|
+
* "Techn. Störung", "Behelfspegel - Messwerte können Fehler aufweisen" (temporary gauge,
|
|
66
|
+
* values may be wrong), "vorübergehend außer Betrieb". A current measurement whose state
|
|
67
|
+
* is `commented` points here.
|
|
68
|
+
*/
|
|
69
|
+
export interface TimeseriesComment {
|
|
70
|
+
shortDescription: string;
|
|
71
|
+
longDescription?: string;
|
|
72
|
+
}
|
|
44
73
|
/** Metadata for one timeseries of a station (e.g. "W" water level, "Q" flow). */
|
|
45
74
|
export interface TimeseriesInfo {
|
|
46
75
|
shortname: string;
|
|
47
76
|
longname: string;
|
|
77
|
+
/**
|
|
78
|
+
* The unit of every value of this series, as the API publishes it. Read it; never
|
|
79
|
+
* assume it from the shortname: most `W` (water level) series are in `cm`, but canal
|
|
80
|
+
* and reservoir gauges publish `W` in `m+NN` (metres above sea level) or `m+PNP`
|
|
81
|
+
* (metres above the gauge zero) — 69 of 737 W series on 5 October 2026, e.g. MÜNSTER OW
|
|
82
|
+
* at 56.54 m+NN. `Q` is usually `m³/s`, temperatures `°C`.
|
|
83
|
+
*/
|
|
48
84
|
unit: string;
|
|
49
85
|
equidistance?: number;
|
|
50
86
|
/** Gauge zero of a water-level series (absent on e.g. flow series). */
|
|
51
87
|
gaugeZero?: GaugeZero;
|
|
88
|
+
/**
|
|
89
|
+
* The operator's note while the series is disturbed (see {@link TimeseriesComment});
|
|
90
|
+
* absent otherwise. Read it whenever a reading's state is `commented`.
|
|
91
|
+
*/
|
|
92
|
+
comment?: TimeseriesComment;
|
|
52
93
|
currentMeasurement?: CurrentMeasurement;
|
|
53
94
|
/**
|
|
54
95
|
* Characteristic values (gauge marks), present only when requested; an empty
|
|
@@ -59,14 +100,24 @@ export interface TimeseriesInfo {
|
|
|
59
100
|
/** One point of a measurements series. */
|
|
60
101
|
export interface Measurement {
|
|
61
102
|
timestamp: string;
|
|
62
|
-
|
|
103
|
+
/**
|
|
104
|
+
* The reading in the timeseries' unit, or `null` for a point without a value (the
|
|
105
|
+
* sentinel `99999`, mapped by the client). Skip `null` points before a minimum, maximum
|
|
106
|
+
* or trend.
|
|
107
|
+
*/
|
|
108
|
+
value: number | null;
|
|
63
109
|
}
|
|
64
110
|
/** Parameters for the stations listing. */
|
|
65
111
|
export interface StationListParams {
|
|
66
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Station identifiers (uuid/number/shortname/longname); sent comma-separated.
|
|
114
|
+
* At least one, none blank. A name may match more than one station (NEUSTADT: LEINE
|
|
115
|
+
* and OSTSEE) and an unknown one matches none, silently: `stationListNotes` reports both.
|
|
116
|
+
*/
|
|
67
117
|
ids?: string[];
|
|
68
|
-
/** Water shortname filter. */
|
|
118
|
+
/** Water shortname filter; not blank. */
|
|
69
119
|
waters?: string;
|
|
120
|
+
/** Fuzzy id match; not blank. */
|
|
70
121
|
fuzzyId?: string;
|
|
71
122
|
/** Embed each station's timeseries list. */
|
|
72
123
|
includeTimeseries?: boolean;
|
|
@@ -90,9 +141,8 @@ export interface IncludeParams {
|
|
|
90
141
|
includeCurrentMeasurement?: boolean;
|
|
91
142
|
includeCharacteristicValues?: boolean;
|
|
92
143
|
}
|
|
93
|
-
/** Time window for a measurements request (ISO-8601 instants or periods, e.g. "P7D"). */
|
|
144
|
+
/** Time window for a measurements request (ISO-8601 instants or periods, e.g. "P7D"); neither bound may be blank. */
|
|
94
145
|
export interface MeasurementsParams {
|
|
95
146
|
start?: string;
|
|
96
147
|
end?: string;
|
|
97
148
|
}
|
|
98
|
-
//# sourceMappingURL=types.d.ts.map
|
package/dist/src/client/types.js
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** A rule: the reason `value` is invalid (e.g. `"Expected a non-empty value."`), or `undefined` when it is valid. */
|
|
2
|
+
export type Problem<T = unknown> = (value: T) => string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Throw a {@link PegelValidationError} with the message `Invalid <name>: <reason>`
|
|
5
|
+
* when `problem(value)` finds a reason; otherwise return `value` unchanged. Call it
|
|
6
|
+
* before any request, so a rejected input sends nothing. Async methods call it
|
|
7
|
+
* inside their body, so the rejection arrives as a rejected promise rather than a
|
|
8
|
+
* synchronous throw.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assertValid<T>(name: string, value: T, problem: Problem<T>): T;
|
|
11
|
+
/**
|
|
12
|
+
* The form in which an id, name or filter value is sent: surrounding whitespace removed
|
|
13
|
+
* and composed (NFC). The API matches station names, waters and ids exactly, so
|
|
14
|
+
* `"RHEIN "` (a trailing space from a copy) listed no station and `"BONN "` was a 404, and
|
|
15
|
+
* a decomposed umlaut ("KO" + U+0308 + "LN", as pasted from macOS file names or some
|
|
16
|
+
* PDFs) found nothing either; no name upstream begins or ends with whitespace. NFC, not
|
|
17
|
+
* NFKC: an id lookup must not rewrite compatibility characters, and case is left alone
|
|
18
|
+
* (the API ignores it for station ids, but not everywhere).
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeInput(value: string): string;
|
|
21
|
+
/** True for an empty or whitespace-only string. */
|
|
22
|
+
export declare function isBlank(value: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* A filter or query value must be a non-blank string: the API treats an empty
|
|
25
|
+
* parameter (`?waters=`, `?start=`) as no filter, so a blank value would silently
|
|
26
|
+
* return the unfiltered set or the default window.
|
|
27
|
+
*/
|
|
28
|
+
export declare const nonEmptyProblem: Problem<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* The `ids` filter of `stations.list`: at least one id, none of them blank. An
|
|
31
|
+
* empty list would be dropped and list every station; a blank entry would be sent
|
|
32
|
+
* as `ids=BONN,%20`.
|
|
33
|
+
*/
|
|
34
|
+
export declare const idListProblem: Problem<unknown>;
|
|
35
|
+
/**
|
|
36
|
+
* Whitespace and control characters in a base URL. `new URL()` silently trims
|
|
37
|
+
* surrounding whitespace and strips an interior tab or newline, so the URL checks
|
|
38
|
+
* pass, but the engine concatenates request paths onto the raw string:
|
|
39
|
+
* `"https://h/ "` would request `/%20/webservices/...`, and a custom transport would
|
|
40
|
+
* get the raw padded value.
|
|
41
|
+
*/
|
|
42
|
+
export declare const baseUrlWhitespaceProblem: Problem<unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* The full base-URL rule set, in order: no whitespace or control characters
|
|
45
|
+
* (baseUrlWhitespaceProblem), an absolute URL, an `http:`/`https:` scheme, no
|
|
46
|
+
* query or fragment, and no `%` in the userinfo that doesn't start a valid escape
|
|
47
|
+
* (`%25` for a literal one). Request paths are appended to the base URL as a string, so a
|
|
48
|
+
* `?` or `#` in it would swallow every path: `http://h/?x=1` requests
|
|
49
|
+
* `/?x=1/webservices/...` and `http://h/#f` requests `/`. A path prefix is fine, and
|
|
50
|
+
* userinfo is allowed (Node sends it as Basic auth). The reasons name no URL, so a
|
|
51
|
+
* credential in it never reaches a message.
|
|
52
|
+
*/
|
|
53
|
+
export declare const baseUrlProblem: Problem<unknown>;
|
|
54
|
+
/**
|
|
55
|
+
* A value that goes into an HTTP header (the User-Agent): non-blank, no C0 control
|
|
56
|
+
* or DEL (tab is allowed, as in HTTP), nothing above U+00FF. A blank value would be
|
|
57
|
+
* sent as an empty header instead of the default; Node's HTTP layer refuses the
|
|
58
|
+
* others with an opaque "Invalid character in header content" TypeError at request
|
|
59
|
+
* time, and an injected transport would send a CR/LF value as is (header
|
|
60
|
+
* injection). Checked by char code so the source stays free of control bytes.
|
|
61
|
+
*/
|
|
62
|
+
export declare const headerValueProblem: Problem<unknown>;
|
|
63
|
+
/**
|
|
64
|
+
* A parameter object of a client method: a plain object whose own keys are all in
|
|
65
|
+
* `allowed` (an `undefined` value counts as unset and is ignored). A misspelled key
|
|
66
|
+
* (`water` for `waters`, `fuzzyID`), `__proto__` or `constructor` was dropped silently
|
|
67
|
+
* and the API answered with every station; TypeScript catches a typo, JavaScript and a
|
|
68
|
+
* JSON config do not. The reason names the key, a close match and the allowed keys.
|
|
69
|
+
*/
|
|
70
|
+
export declare function knownKeysProblem(allowed: readonly string[]): Problem<unknown>;
|
|
71
|
+
/** An optional flag (`includeTimeseries` …): `true`, `false` or unset — not "yes", 1 or "false". */
|
|
72
|
+
export declare const optionalBooleanProblem: Problem<unknown>;
|