@maschinenlesbar.org/pegel-online-cli 0.0.8 → 0.2.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 +26 -14
- package/dist/src/cli/commands/stations.d.ts +0 -1
- package/dist/src/cli/commands/stations.js +6 -6
- package/dist/src/cli/commands/timeseries.d.ts +0 -1
- package/dist/src/cli/commands/timeseries.js +10 -12
- package/dist/src/cli/index.d.ts +0 -1
- package/dist/src/cli/index.js +2 -1
- package/dist/src/cli/io.d.ts +17 -1
- package/dist/src/cli/io.js +22 -1
- package/dist/src/cli/program.d.ts +0 -1
- package/dist/src/cli/program.js +4 -4
- package/dist/src/cli/run.d.ts +0 -1
- package/dist/src/cli/run.js +7 -2
- package/dist/src/cli/shared.d.ts +25 -14
- package/dist/src/cli/shared.js +60 -34
- package/dist/src/client/client.d.ts +6 -1
- package/dist/src/client/client.js +71 -17
- package/dist/src/client/engine.d.ts +60 -6
- package/dist/src/client/engine.js +157 -54
- package/dist/src/client/errors.d.ts +22 -1
- package/dist/src/client/errors.js +49 -4
- package/dist/src/client/http.d.ts +0 -1
- package/dist/src/client/http.js +3 -4
- package/dist/src/client/index.d.ts +4 -3
- package/dist/src/client/index.js +3 -3
- package/dist/src/client/query.d.ts +0 -1
- package/dist/src/client/query.js +0 -1
- package/dist/src/client/types.d.ts +36 -6
- package/dist/src/client/types.js +0 -1
- package/dist/src/client/validate.d.ts +51 -0
- package/dist/src/client/validate.js +108 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/package.json +2 -1
- 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
package/README.md
CHANGED
|
@@ -86,16 +86,20 @@ level); other codes include `Q` (flow/discharge), `WT` (water temperature), and
|
|
|
86
86
|
| `--waters <shortname>` | filter by water shortname (see `waters`) |
|
|
87
87
|
| `--fuzzy-id <id>` | fuzzy match against short/long name |
|
|
88
88
|
| `--include-timeseries` | embed each station's timeseries list |
|
|
89
|
-
| `--include-current` | embed the current measurement |
|
|
90
|
-
| `--include-characteristic` | embed characteristic (gauge-mark) values |
|
|
89
|
+
| `--include-current` | embed the current measurement in each timeseries (implies `--include-timeseries`) |
|
|
90
|
+
| `--include-characteristic` | embed characteristic (gauge-mark) values in each timeseries (implies `--include-timeseries`) |
|
|
91
91
|
|
|
92
92
|
### `stations get` options
|
|
93
93
|
|
|
94
94
|
| Flag | Meaning |
|
|
95
95
|
| --- | --- |
|
|
96
96
|
| `--include-timeseries` | embed the station's timeseries list |
|
|
97
|
-
| `--include-current` | embed the current measurement |
|
|
98
|
-
| `--include-characteristic` | embed characteristic (gauge-mark) values |
|
|
97
|
+
| `--include-current` | embed the current measurement in each timeseries (implies `--include-timeseries`) |
|
|
98
|
+
| `--include-characteristic` | embed characteristic (gauge-mark) values in each timeseries (implies `--include-timeseries`) |
|
|
99
|
+
|
|
100
|
+
The API nests the current measurement and the gauge marks *inside* each
|
|
101
|
+
timeseries and drops them without the timeseries list, so `--include-current` and
|
|
102
|
+
`--include-characteristic` turn on `--include-timeseries` themselves.
|
|
99
103
|
|
|
100
104
|
### `measurements` options
|
|
101
105
|
|
|
@@ -187,15 +191,23 @@ both `pegel --compact waters` and `pegel waters --compact` do the same thing.
|
|
|
187
191
|
`PATH`. Run `npm bin -g` to find it and add it, or run via
|
|
188
192
|
`npx @maschinenlesbar.org/pegel-online-cli …`.
|
|
189
193
|
- **Exit `2` / "invalid argument"** — check the command syntax: a `<station>`
|
|
190
|
-
argument is required, and
|
|
191
|
-
|
|
192
|
-
- **Exit `4` / "not found"** — the station shortname or id doesn't exist
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
argument is required, and no argument or option value may be blank (or `.` /
|
|
195
|
+
`..` for an id). Run `pegel <command> --help` for the exact signature.
|
|
196
|
+
- **Exit `4` / "not found"** — the station shortname or id doesn't exist, or the
|
|
197
|
+
station doesn't publish the requested series (`Timeseries does not exist.` /
|
|
198
|
+
`Current measurement does not exist.`). Run `pegel stations list --fuzzy-id <name>`
|
|
199
|
+
or `pegel waters` to find the right shortname, and
|
|
200
|
+
`pegel stations get <station> --include-timeseries | jq -r '.timeseries[].shortname'`
|
|
201
|
+
to see which series codes it exposes.
|
|
202
|
+
- **Exit `1` / `HTTP 400` on `measurements`** — `--start` / `--end` are not checked
|
|
203
|
+
locally; the API rejects a value that is not an ISO-8601 instant or period
|
|
204
|
+
(`Given start parameter is neither a valid ISO date time, nor an ISO period.`) and
|
|
205
|
+
a window whose start is not before its end (e.g. a start in the future). Use
|
|
206
|
+
`P7D`, not `7d`.
|
|
207
|
+
- **Empty `[]` from `measurements`** — the window lies outside the data the API
|
|
208
|
+
keeps (about the last month), e.g. a date from earlier in the year.
|
|
195
209
|
- **Exit `1` / network error** — connectivity, DNS, or a timeout. Try again, or
|
|
196
210
|
raise the limit with `--timeout 60000`.
|
|
197
|
-
- **Empty `timeseries` array** — the station doesn't publish the requested series.
|
|
198
|
-
Run `pegel timeseries <station>` to see which codes it actually exposes.
|
|
199
211
|
|
|
200
212
|
## Global options
|
|
201
213
|
|
|
@@ -206,10 +218,10 @@ These apply to every command and may be given before *or* after it:
|
|
|
206
218
|
| `-V, --version` | Print the version number |
|
|
207
219
|
| `-h, --help` | Show help for the program or a command |
|
|
208
220
|
| `--compact` | Print JSON on a single line instead of pretty-printed |
|
|
209
|
-
| `--base-url <url>` | API base URL (default `https://www.pegelonline.wsv.de`) |
|
|
221
|
+
| `--base-url <url>` | API base URL (default `https://www.pegelonline.wsv.de`); http(s) only, a path prefix is fine, no query (`?`), fragment (`#`), whitespace or control characters; userinfo is sent as Basic auth but shown as `***` in messages |
|
|
210
222
|
| `--timeout <ms>` | Time limit per request in milliseconds, reading the whole response included (default `30000`; at most `2147483647`) |
|
|
211
|
-
| `--user-agent <ua>` | `User-Agent` header value |
|
|
212
|
-
| `--max-retries <n>` | Retries for transient `429`/`503` responses (default `2`) |
|
|
223
|
+
| `--user-agent <ua>` | `User-Agent` header value (not blank; Latin-1, no control characters) |
|
|
224
|
+
| `--max-retries <n>` | Retries for transient `429`/`503` responses, `0`–`10` (default `2`); each waits the server's `Retry-After` (up to 30 s; a longer one is not retried), else 200 ms × attempt |
|
|
213
225
|
| `--max-response-bytes <n>` | Cap response body size in bytes (`0` = unlimited; default 100 MiB) |
|
|
214
226
|
|
|
215
227
|
## Learn more
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
|
-
import { action, parseNonEmpty,
|
|
2
|
+
import { STATION_HELP, action, parseNonEmpty, parsePathArg, renderJson } from "../shared.js";
|
|
3
3
|
/** commander accumulator for a repeatable string option. */
|
|
4
4
|
function collect(value, previous = []) {
|
|
5
5
|
return previous.concat([parseNonEmpty(value)]);
|
|
@@ -15,8 +15,8 @@ function includesFrom(opts) {
|
|
|
15
15
|
function addIncludeOptions(cmd) {
|
|
16
16
|
return cmd
|
|
17
17
|
.addOption(new Option("--include-timeseries", "embed each station's timeseries list"))
|
|
18
|
-
.addOption(new Option("--include-current", "embed the current measurement"))
|
|
19
|
-
.addOption(new Option("--include-characteristic", "embed characteristic (gauge-mark) values"));
|
|
18
|
+
.addOption(new Option("--include-current", "embed the current measurement in each timeseries (implies --include-timeseries)"))
|
|
19
|
+
.addOption(new Option("--include-characteristic", "embed characteristic (gauge-mark) values in each timeseries (implies --include-timeseries)"));
|
|
20
20
|
}
|
|
21
21
|
export function registerStationCommands(program, deps) {
|
|
22
22
|
const stations = program.command("stations").description("Measuring stations");
|
|
@@ -36,10 +36,10 @@ export function registerStationCommands(program, deps) {
|
|
|
36
36
|
renderJson(deps, global, await client.stations.list(params));
|
|
37
37
|
}));
|
|
38
38
|
const get = stations
|
|
39
|
-
.command("get
|
|
39
|
+
.command("get")
|
|
40
|
+
.argument("<station>", STATION_HELP, parsePathArg)
|
|
40
41
|
.description("Get one station by uuid/number/shortname/longname");
|
|
41
42
|
addIncludeOptions(get).action(action(deps, async ({ client, global, opts }, [station]) => {
|
|
42
|
-
renderJson(deps, global, await client.stations.get(
|
|
43
|
+
renderJson(deps, global, await client.stations.get(station, includesFrom(opts)));
|
|
43
44
|
}));
|
|
44
45
|
}
|
|
45
|
-
//# sourceMappingURL=stations.js.map
|
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
import { action, parseNonEmpty,
|
|
2
|
-
const STATION_HELP = "station uuid, number, shortname or longname";
|
|
1
|
+
import { STATION_HELP, action, parseNonEmpty, parsePathArg, renderJson, timeseriesOr } from "../shared.js";
|
|
3
2
|
const TIMESERIES_HELP = "timeseries shortname, e.g. W (water level) or Q (flow)";
|
|
4
3
|
export function registerTimeseriesCommands(program, deps) {
|
|
5
4
|
program
|
|
6
5
|
.command("timeseries")
|
|
7
|
-
.argument("<station>", STATION_HELP)
|
|
8
|
-
.argument("[timeseries]", TIMESERIES_HELP,
|
|
6
|
+
.argument("<station>", STATION_HELP, parsePathArg)
|
|
7
|
+
.argument("[timeseries]", TIMESERIES_HELP, parsePathArg)
|
|
9
8
|
.description("Timeseries metadata (timeseries defaults to 'W' = water level)")
|
|
10
9
|
.action(action(deps, async ({ client, global }, [station, ts]) => {
|
|
11
|
-
renderJson(deps, global, await client.timeseries.get(
|
|
10
|
+
renderJson(deps, global, await client.timeseries.get(station, timeseriesOr(ts)));
|
|
12
11
|
}));
|
|
13
12
|
program
|
|
14
13
|
.command("current")
|
|
15
|
-
.argument("<station>", STATION_HELP)
|
|
16
|
-
.argument("[timeseries]", TIMESERIES_HELP,
|
|
14
|
+
.argument("<station>", STATION_HELP, parsePathArg)
|
|
15
|
+
.argument("[timeseries]", TIMESERIES_HELP, parsePathArg)
|
|
17
16
|
.description("The current measurement (timeseries defaults to 'W')")
|
|
18
17
|
.action(action(deps, async ({ client, global }, [station, ts]) => {
|
|
19
|
-
renderJson(deps, global, await client.timeseries.currentMeasurement(
|
|
18
|
+
renderJson(deps, global, await client.timeseries.currentMeasurement(station, timeseriesOr(ts)));
|
|
20
19
|
}));
|
|
21
20
|
program
|
|
22
21
|
.command("measurements")
|
|
23
|
-
.argument("<station>", STATION_HELP)
|
|
24
|
-
.argument("[timeseries]", TIMESERIES_HELP,
|
|
22
|
+
.argument("<station>", STATION_HELP, parsePathArg)
|
|
23
|
+
.argument("[timeseries]", TIMESERIES_HELP, parsePathArg)
|
|
25
24
|
.description("A window of measurements (timeseries defaults to 'W')")
|
|
26
25
|
.option("--start <iso>", "window start: ISO-8601 instant, or a period like P7D", parseNonEmpty)
|
|
27
26
|
.option("--end <iso>", "window end: ISO-8601 instant", parseNonEmpty)
|
|
28
27
|
.action(action(deps, async ({ client, global, opts }, [station, ts]) => {
|
|
29
|
-
renderJson(deps, global, await client.timeseries.measurements(
|
|
28
|
+
renderJson(deps, global, await client.timeseries.measurements(station, timeseriesOr(ts), {
|
|
30
29
|
start: opts["start"],
|
|
31
30
|
end: opts["end"],
|
|
32
31
|
}));
|
|
@@ -38,4 +37,3 @@ export function registerTimeseriesCommands(program, deps) {
|
|
|
38
37
|
renderJson(deps, global, await client.waters());
|
|
39
38
|
}));
|
|
40
39
|
}
|
|
41
|
-
//# sourceMappingURL=timeseries.js.map
|
package/dist/src/cli/index.d.ts
CHANGED
package/dist/src/cli/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Binary entry point. Thin shim around run(); all logic lives in run.ts/program.ts.
|
|
3
|
+
import { handleOutputErrors } from "./io.js";
|
|
3
4
|
import { run } from "./run.js";
|
|
5
|
+
handleOutputErrors();
|
|
4
6
|
const exitCode = await run(process.argv.slice(2));
|
|
5
7
|
process.exitCode = exitCode;
|
|
6
|
-
//# sourceMappingURL=index.js.map
|
package/dist/src/cli/io.d.ts
CHANGED
|
@@ -9,5 +9,21 @@ export interface CliDeps {
|
|
|
9
9
|
/** Build a client from the resolved global options (injectable for tests). */
|
|
10
10
|
createClient(options: EngineOptions): PegelOnlineClient;
|
|
11
11
|
}
|
|
12
|
+
/** The two process streams, as far as `handleOutputErrors` needs them. */
|
|
13
|
+
export interface OutputStreams {
|
|
14
|
+
stdout: Pick<NodeJS.WriteStream, "on">;
|
|
15
|
+
stderr: Pick<NodeJS.WriteStream, "on">;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Handle write errors on stdout/stderr, which Node otherwise reports as an
|
|
19
|
+
* unhandled 'error' event: a raw stack trace and exit 1.
|
|
20
|
+
*
|
|
21
|
+
* A reader that stops early — `| head`, `| jq` exiting on the first match, a closed
|
|
22
|
+
* pager — closes the pipe while the CLI is still writing, and the next write fails
|
|
23
|
+
* with EPIPE. That is ordinary use, so the process exits 0 at once, quietly. Any
|
|
24
|
+
* other stdout error prints one `Output error: <message>` line to stderr and exits
|
|
25
|
+
* 1; any other stderr error exits 1 silently (there is nowhere left to report it).
|
|
26
|
+
* The bin shim installs this once, before `run()`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function handleOutputErrors(streams?: OutputStreams, exit?: (code: number) => void): void;
|
|
12
29
|
export declare const defaultIO: CliIO;
|
|
13
|
-
//# sourceMappingURL=io.d.ts.map
|
package/dist/src/cli/io.js
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
// I/O seam for the CLI. Everything the CLI writes goes through a CliIO object so
|
|
2
2
|
// tests can capture output instead of hitting the real stdout/stderr.
|
|
3
|
+
/**
|
|
4
|
+
* Handle write errors on stdout/stderr, which Node otherwise reports as an
|
|
5
|
+
* unhandled 'error' event: a raw stack trace and exit 1.
|
|
6
|
+
*
|
|
7
|
+
* A reader that stops early — `| head`, `| jq` exiting on the first match, a closed
|
|
8
|
+
* pager — closes the pipe while the CLI is still writing, and the next write fails
|
|
9
|
+
* with EPIPE. That is ordinary use, so the process exits 0 at once, quietly. Any
|
|
10
|
+
* other stdout error prints one `Output error: <message>` line to stderr and exits
|
|
11
|
+
* 1; any other stderr error exits 1 silently (there is nowhere left to report it).
|
|
12
|
+
* The bin shim installs this once, before `run()`.
|
|
13
|
+
*/
|
|
14
|
+
export function handleOutputErrors(streams = process, exit = (code) => process.exit(code)) {
|
|
15
|
+
streams.stdout.on("error", (err) => {
|
|
16
|
+
if (err.code === "EPIPE")
|
|
17
|
+
return exit(0);
|
|
18
|
+
process.stderr.write(`Output error: ${err.message}\n`);
|
|
19
|
+
exit(1);
|
|
20
|
+
});
|
|
21
|
+
streams.stderr.on("error", (err) => {
|
|
22
|
+
exit(err.code === "EPIPE" ? 0 : 1);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
3
25
|
export const defaultIO = {
|
|
4
26
|
out: (text) => process.stdout.write(text + "\n"),
|
|
5
27
|
err: (text) => process.stderr.write(text + "\n"),
|
|
6
28
|
};
|
|
7
|
-
//# sourceMappingURL=io.js.map
|
package/dist/src/cli/program.js
CHANGED
|
@@ -7,7 +7,8 @@ import { Command } from "commander";
|
|
|
7
7
|
import { defaultIO } from "./io.js";
|
|
8
8
|
import { PegelOnlineClient } from "../client/client.js";
|
|
9
9
|
import { MAX_TIMEOUT_MS } from "../client/http.js";
|
|
10
|
-
import {
|
|
10
|
+
import { MAX_RETRIES } from "../client/engine.js";
|
|
11
|
+
import { parseBoundedInt, parseIntArg, parseBaseUrl, parseHeaderValue } from "./shared.js";
|
|
11
12
|
import { registerStationCommands } from "./commands/stations.js";
|
|
12
13
|
import { registerTimeseriesCommands } from "./commands/timeseries.js";
|
|
13
14
|
/**
|
|
@@ -41,8 +42,8 @@ export function buildProgram(deps = defaultDeps) {
|
|
|
41
42
|
.version(VERSION)
|
|
42
43
|
.option("--base-url <url>", "API base URL", parseBaseUrl, "https://www.pegelonline.wsv.de")
|
|
43
44
|
.option("--timeout <ms>", "time limit per request in milliseconds, whole response included", parseBoundedInt(0, MAX_TIMEOUT_MS))
|
|
44
|
-
.option("--user-agent <ua>", "User-Agent header value")
|
|
45
|
-
.option("--max-retries <n>", "retries for transient 429/503 responses",
|
|
45
|
+
.option("--user-agent <ua>", "User-Agent header value", parseHeaderValue)
|
|
46
|
+
.option("--max-retries <n>", "retries for transient 429/503 responses (0..10; each waits the server's Retry-After, up to 30 s)", parseBoundedInt(0, MAX_RETRIES))
|
|
46
47
|
.option("--max-response-bytes <n>", "cap response body size in bytes (0 = unlimited; default 100 MiB)", parseIntArg)
|
|
47
48
|
.option("--compact", "print JSON on a single line instead of pretty-printed")
|
|
48
49
|
.showHelpAfterError();
|
|
@@ -50,4 +51,3 @@ export function buildProgram(deps = defaultDeps) {
|
|
|
50
51
|
registerTimeseriesCommands(program, deps);
|
|
51
52
|
return program;
|
|
52
53
|
}
|
|
53
|
-
//# sourceMappingURL=program.js.map
|
package/dist/src/cli/run.d.ts
CHANGED
package/dist/src/cli/run.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// captured output and exit code without spawning a subprocess.
|
|
4
4
|
import { CommanderError } from "commander";
|
|
5
5
|
import { buildProgram, defaultDeps } from "./program.js";
|
|
6
|
-
import { PegelApiError, PegelError } from "../client/errors.js";
|
|
6
|
+
import { PegelApiError, PegelError, PegelValidationError } from "../client/errors.js";
|
|
7
7
|
/**
|
|
8
8
|
* Apply exitOverride + output redirection to every command in the tree.
|
|
9
9
|
* commander does not propagate these to subcommands, so a parse error on a
|
|
@@ -51,6 +51,12 @@ export async function run(argv, deps = defaultDeps) {
|
|
|
51
51
|
return 4;
|
|
52
52
|
return 1;
|
|
53
53
|
}
|
|
54
|
+
if (err instanceof PegelValidationError) {
|
|
55
|
+
// An input the library rejected before any request: a usage error, the same
|
|
56
|
+
// exit code as commander's own parse errors.
|
|
57
|
+
deps.io.err(`Error: ${err.message}`);
|
|
58
|
+
return USAGE_EXIT;
|
|
59
|
+
}
|
|
54
60
|
if (err instanceof PegelError) {
|
|
55
61
|
deps.io.err(`Error: ${err.message}`);
|
|
56
62
|
return 1;
|
|
@@ -59,4 +65,3 @@ export async function run(argv, deps = defaultDeps) {
|
|
|
59
65
|
return 1;
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
|
-
//# sourceMappingURL=run.js.map
|
package/dist/src/cli/shared.d.ts
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
import type { CliDeps } from "./io.js";
|
|
2
2
|
import type { EngineOptions } from "../client/engine.js";
|
|
3
|
+
/** Help text of every `<station>` positional. */
|
|
4
|
+
export declare const STATION_HELP = "station uuid, number, shortname or longname";
|
|
3
5
|
/** commander value-parser: a non-negative integer. */
|
|
4
6
|
export declare function parseIntArg(value: string): number;
|
|
5
7
|
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
6
8
|
export declare function parseBoundedInt(min: number, max: number): (value: string) => number;
|
|
7
9
|
/**
|
|
8
|
-
* commander value-parser: a value that is not blank.
|
|
9
|
-
*
|
|
10
|
+
* commander value-parser: a value that is not blank. The rule is the library's
|
|
11
|
+
* nonEmptyProblem, which the client enforces on every filter value too (the API
|
|
12
|
+
* reads an empty parameter as no filter); here it only turns a blank value into an
|
|
13
|
+
* early usage error.
|
|
10
14
|
*/
|
|
11
15
|
export declare function parseNonEmpty(value: string): string;
|
|
12
16
|
/**
|
|
13
|
-
* commander value-parser for
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* enforces the same allowlist as the authoritative egress control; this only moves
|
|
18
|
-
* the user-facing rejection earlier and to the correct exit code.
|
|
17
|
+
* commander value-parser for a value that ends up in an HTTP header (`--user-agent`):
|
|
18
|
+
* the library's headerValueProblem rule (not blank, no control character except tab,
|
|
19
|
+
* nothing above U+00FF), which the engine enforces when the client is built; here it
|
|
20
|
+
* only turns a bad value into an early usage error.
|
|
19
21
|
*/
|
|
20
|
-
export declare function
|
|
22
|
+
export declare function parseHeaderValue(value: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* commander value-parser for an id that becomes a URL path segment (the
|
|
25
|
+
* `<station>` and `[timeseries]` positionals): not blank (which would build
|
|
26
|
+
* `/stations//W/...`), and not "." / "..", which
|
|
27
|
+
* encodeURIComponent leaves untouched and URL parsing would resolve, sending the
|
|
28
|
+
* request to a different resource. A usage error, before any request.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parsePathArg(value: string): string;
|
|
21
31
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
32
|
+
* commander value-parser for `--base-url`: the library's baseUrlProblem rule (no
|
|
33
|
+
* whitespace or control characters, an absolute http(s) URL, no query or
|
|
34
|
+
* fragment), reported at *parse* time as a usage error (exit 2). The engine
|
|
35
|
+
* enforces the same rule when the client is built, and the transport still gates
|
|
36
|
+
* the scheme on every hop as the authoritative egress control.
|
|
25
37
|
*/
|
|
26
|
-
export declare function
|
|
38
|
+
export declare function parseBaseUrl(value: string): string;
|
|
27
39
|
/**
|
|
28
40
|
* Default an omitted optional `[timeseries]` positional to "W" (water level),
|
|
29
41
|
* matching the documented default. A blank value never reaches here: the
|
|
@@ -65,4 +77,3 @@ export interface ActionContext {
|
|
|
65
77
|
* the trailing options object and command instance to recover the positionals.
|
|
66
78
|
*/
|
|
67
79
|
export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
|
|
68
|
-
//# sourceMappingURL=shared.d.ts.map
|
package/dist/src/cli/shared.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// option resolver, and the JSON result renderer.
|
|
3
3
|
import { InvalidArgumentError } from "commander";
|
|
4
4
|
import { PegelError } from "../client/errors.js";
|
|
5
|
+
import { baseUrlProblem, headerValueProblem, nonEmptyProblem } from "../client/validate.js";
|
|
6
|
+
/** Help text of every `<station>` positional. */
|
|
7
|
+
export const STATION_HELP = "station uuid, number, shortname or longname";
|
|
5
8
|
/** commander value-parser: a non-negative integer. */
|
|
6
9
|
export function parseIntArg(value) {
|
|
7
10
|
// Require a plain decimal integer. Reject blank/whitespace ("" and " " coerce
|
|
@@ -28,52 +31,56 @@ export function parseBoundedInt(min, max) {
|
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
|
-
* commander value-parser: a value that is not blank.
|
|
32
|
-
*
|
|
34
|
+
* commander value-parser: a value that is not blank. The rule is the library's
|
|
35
|
+
* nonEmptyProblem, which the client enforces on every filter value too (the API
|
|
36
|
+
* reads an empty parameter as no filter); here it only turns a blank value into an
|
|
37
|
+
* early usage error.
|
|
33
38
|
*/
|
|
34
39
|
export function parseNonEmpty(value) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const reason = nonEmptyProblem(value);
|
|
41
|
+
if (reason !== undefined)
|
|
42
|
+
throw new InvalidArgumentError(reason);
|
|
38
43
|
return value;
|
|
39
44
|
}
|
|
40
45
|
/**
|
|
41
|
-
* commander value-parser for
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* enforces the same allowlist as the authoritative egress control; this only moves
|
|
46
|
-
* the user-facing rejection earlier and to the correct exit code.
|
|
46
|
+
* commander value-parser for a value that ends up in an HTTP header (`--user-agent`):
|
|
47
|
+
* the library's headerValueProblem rule (not blank, no control character except tab,
|
|
48
|
+
* nothing above U+00FF), which the engine enforces when the client is built; here it
|
|
49
|
+
* only turns a bad value into an early usage error.
|
|
47
50
|
*/
|
|
48
|
-
export function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
throw new InvalidArgumentError("Expected an absolute http(s) URL.");
|
|
55
|
-
}
|
|
56
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
57
|
-
throw new InvalidArgumentError("Only http and https URLs are supported.");
|
|
58
|
-
}
|
|
51
|
+
export function parseHeaderValue(value) {
|
|
52
|
+
const reason = headerValueProblem(value);
|
|
53
|
+
if (reason !== undefined)
|
|
54
|
+
throw new InvalidArgumentError(reason);
|
|
59
55
|
return value;
|
|
60
56
|
}
|
|
61
57
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
58
|
+
* commander value-parser for an id that becomes a URL path segment (the
|
|
59
|
+
* `<station>` and `[timeseries]` positionals): not blank (which would build
|
|
60
|
+
* `/stations//W/...`), and not "." / "..", which
|
|
61
|
+
* encodeURIComponent leaves untouched and URL parsing would resolve, sending the
|
|
62
|
+
* request to a different resource. A usage error, before any request.
|
|
65
63
|
*/
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
throw new PegelError(`Missing required <${name}> argument.`);
|
|
69
|
-
}
|
|
70
|
-
// Reject "." / ".." which encodeURIComponent leaves untouched and which would
|
|
71
|
-
// otherwise inject a relative path segment into the request URL.
|
|
64
|
+
export function parsePathArg(value) {
|
|
65
|
+
parseNonEmpty(value);
|
|
72
66
|
if (value === "." || value === "..") {
|
|
73
|
-
throw new
|
|
67
|
+
throw new InvalidArgumentError('"." and ".." cannot be used as an id.');
|
|
74
68
|
}
|
|
75
69
|
return value;
|
|
76
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* commander value-parser for `--base-url`: the library's baseUrlProblem rule (no
|
|
73
|
+
* whitespace or control characters, an absolute http(s) URL, no query or
|
|
74
|
+
* fragment), reported at *parse* time as a usage error (exit 2). The engine
|
|
75
|
+
* enforces the same rule when the client is built, and the transport still gates
|
|
76
|
+
* the scheme on every hop as the authoritative egress control.
|
|
77
|
+
*/
|
|
78
|
+
export function parseBaseUrl(value) {
|
|
79
|
+
const reason = baseUrlProblem(value);
|
|
80
|
+
if (reason !== undefined)
|
|
81
|
+
throw new InvalidArgumentError(reason);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
77
84
|
/**
|
|
78
85
|
* Default an omitted optional `[timeseries]` positional to "W" (water level),
|
|
79
86
|
* matching the documented default. A blank value never reaches here: the
|
|
@@ -116,9 +123,29 @@ export function escapeControlChars(json) {
|
|
|
116
123
|
}
|
|
117
124
|
return from === 0 ? json : result + json.slice(from);
|
|
118
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* JSON.stringify, pretty or compact. A deeply nested value (a hostile or broken
|
|
128
|
+
* response) overflows the stack — the pretty form far sooner than the compact one,
|
|
129
|
+
* which is why the message suggests --compact. The RangeError becomes a PegelError so
|
|
130
|
+
* the CLI prints a clear message instead of "Unexpected error: Maximum call stack
|
|
131
|
+
* size exceeded".
|
|
132
|
+
*/
|
|
133
|
+
function stringifyJson(value, compact) {
|
|
134
|
+
try {
|
|
135
|
+
return compact ? JSON.stringify(value) : JSON.stringify(value, null, 2);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof RangeError) {
|
|
139
|
+
throw new PegelError(compact
|
|
140
|
+
? "The response is nested too deeply to print."
|
|
141
|
+
: "The response is nested too deeply to pretty-print; try --compact.", { cause: err });
|
|
142
|
+
}
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
119
146
|
/** Render a JSON value to stdout, pretty by default, compact with --compact. */
|
|
120
147
|
export function renderJson(deps, global, value) {
|
|
121
|
-
const text = escapeControlChars(
|
|
148
|
+
const text = escapeControlChars(stringifyJson(value, global.compact === true));
|
|
122
149
|
deps.io.out(text);
|
|
123
150
|
}
|
|
124
151
|
/**
|
|
@@ -138,4 +165,3 @@ export function action(deps, fn) {
|
|
|
138
165
|
await fn({ client, global, opts: command.opts() }, positionals);
|
|
139
166
|
};
|
|
140
167
|
}
|
|
141
|
-
//# sourceMappingURL=shared.js.map
|
|
@@ -4,6 +4,11 @@ import type { Station, Water, TimeseriesInfo, CurrentMeasurement, Measurement, S
|
|
|
4
4
|
declare class StationsResource {
|
|
5
5
|
private readonly e;
|
|
6
6
|
constructor(e: RequestEngine);
|
|
7
|
+
/**
|
|
8
|
+
* Rejects (PegelValidationError, no request) a blank `waters` or `fuzzyId`, a
|
|
9
|
+
* blank `ids` entry and an empty `ids` array: the API reads an empty parameter as
|
|
10
|
+
* no filter and would answer with every station.
|
|
11
|
+
*/
|
|
7
12
|
list(params?: StationListParams): Promise<Station[]>;
|
|
8
13
|
get(station: string, params?: IncludeParams): Promise<Station>;
|
|
9
14
|
}
|
|
@@ -14,6 +19,7 @@ declare class TimeseriesResource {
|
|
|
14
19
|
/** Timeseries metadata (e.g. "W" = water level, "Q" = flow). */
|
|
15
20
|
get(station: string, timeseries?: string, params?: IncludeParams): Promise<TimeseriesInfo>;
|
|
16
21
|
currentMeasurement(station: string, timeseries?: string): Promise<CurrentMeasurement>;
|
|
22
|
+
/** Rejects (PegelValidationError, no request) a blank `start` or `end`. */
|
|
17
23
|
measurements(station: string, timeseries?: string, params?: MeasurementsParams): Promise<Measurement[]>;
|
|
18
24
|
}
|
|
19
25
|
export declare class PegelOnlineClient {
|
|
@@ -25,4 +31,3 @@ export declare class PegelOnlineClient {
|
|
|
25
31
|
waters(): Promise<Water[]>;
|
|
26
32
|
}
|
|
27
33
|
export {};
|
|
28
|
-
//# sourceMappingURL=client.d.ts.map
|