@maschinenlesbar.org/marktstammdatenregister-cli 0.0.8 → 0.1.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 +1 -0
- package/dist/src/cli/commands/units.d.ts +0 -1
- package/dist/src/cli/commands/units.js +19 -15
- package/dist/src/cli/index.d.ts +0 -1
- package/dist/src/cli/index.js +0 -1
- package/dist/src/cli/io.d.ts +0 -1
- package/dist/src/cli/io.js +0 -1
- package/dist/src/cli/program.d.ts +0 -1
- package/dist/src/cli/program.js +2 -2
- package/dist/src/cli/run.d.ts +0 -1
- package/dist/src/cli/run.js +0 -1
- package/dist/src/cli/shared.d.ts +20 -16
- package/dist/src/cli/shared.js +26 -54
- package/dist/src/client/client.d.ts +13 -4
- package/dist/src/client/client.js +23 -3
- package/dist/src/client/engine.d.ts +41 -10
- package/dist/src/client/engine.js +54 -31
- package/dist/src/client/errors.d.ts +3 -2
- package/dist/src/client/errors.js +3 -2
- package/dist/src/client/filter.d.ts +0 -1
- package/dist/src/client/filter.js +0 -1
- package/dist/src/client/http.d.ts +0 -1
- package/dist/src/client/http.js +43 -33
- package/dist/src/client/index.d.ts +3 -2
- package/dist/src/client/index.js +2 -2
- package/dist/src/client/query.d.ts +0 -1
- package/dist/src/client/query.js +0 -1
- package/dist/src/client/types.d.ts +2 -1
- package/dist/src/client/types.js +0 -1
- package/dist/src/client/validate.d.ts +63 -0
- package/dist/src/client/validate.js +131 -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/units.d.ts.map +0 -1
- package/dist/src/cli/commands/units.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/filter.d.ts.map +0 -1
- package/dist/src/client/filter.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
|
@@ -67,6 +67,7 @@ import { MastrClient, parseMsDate } from "@maschinenlesbar.org/marktstammdatenre
|
|
|
67
67
|
const mastr = new MastrClient();
|
|
68
68
|
const solar = await mastr.stromerzeugung({ filter: "Energieträger~eq~'2495'", pageSize: 10 });
|
|
69
69
|
solar.total; // total matching units
|
|
70
|
+
await mastr.count("stromerzeugung", { filter: "Energieträger~eq~'2495'" }); // just the count, one-row request
|
|
70
71
|
parseMsDate(String(solar.data[0]?.EinheitRegistrierungsdatum)); // Date | null
|
|
71
72
|
```
|
|
72
73
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// `filters` command to discover the filterable columns and their dropdown codes.
|
|
4
4
|
import { Argument } from "commander";
|
|
5
5
|
import { MAX_PAGE, MAX_PAGE_SIZE } from "../../client/client.js";
|
|
6
|
-
import { action, parseBoundedInt, parseFilter,
|
|
6
|
+
import { action, parseBoundedInt, parseFilter, parseSort, renderJson } from "../shared.js";
|
|
7
7
|
// `sortKey` is a record field that exists in that category's rows (live 2026-09-26):
|
|
8
8
|
// only stromerzeugung rows carry Bruttoleistung/Nettonennleistung, so the stderr hint
|
|
9
9
|
// must not suggest it for the other three.
|
|
@@ -19,21 +19,22 @@ const RUN = {
|
|
|
19
19
|
gaserzeugung: (c, q) => c.gaserzeugung(q),
|
|
20
20
|
gasverbrauch: (c, q) => c.gasverbrauch(q),
|
|
21
21
|
};
|
|
22
|
+
/** Build a CountQuery (filter and sort) from this command's parsed options. */
|
|
23
|
+
function buildCountQuery(opts) {
|
|
24
|
+
const q = {};
|
|
25
|
+
if (typeof opts["sort"] === "string")
|
|
26
|
+
q.sort = opts["sort"];
|
|
27
|
+
if (typeof opts["filter"] === "string")
|
|
28
|
+
q.filter = opts["filter"];
|
|
29
|
+
return q;
|
|
30
|
+
}
|
|
22
31
|
/** Build a UnitQuery from this command's parsed options. */
|
|
23
32
|
function buildQuery(opts) {
|
|
24
|
-
const q =
|
|
33
|
+
const q = buildCountQuery(opts);
|
|
25
34
|
if (typeof opts["page"] === "number")
|
|
26
35
|
q.page = opts["page"];
|
|
27
36
|
if (typeof opts["pageSize"] === "number")
|
|
28
37
|
q.pageSize = opts["pageSize"];
|
|
29
|
-
if (typeof opts["sort"] === "string")
|
|
30
|
-
q.sort = opts["sort"];
|
|
31
|
-
if (typeof opts["filter"] === "string")
|
|
32
|
-
q.filter = opts["filter"];
|
|
33
|
-
// `--total` needs only the match count (returned regardless of page size), so
|
|
34
|
-
// request a single row instead of fetching and discarding a full page.
|
|
35
|
-
if (opts["total"] === true)
|
|
36
|
-
q.pageSize = 1;
|
|
37
38
|
return q;
|
|
38
39
|
}
|
|
39
40
|
export function registerCommands(program, deps) {
|
|
@@ -44,13 +45,17 @@ export function registerCommands(program, deps) {
|
|
|
44
45
|
.option("--page <n>", "1-based page number", parseBoundedInt(1, MAX_PAGE), 1)
|
|
45
46
|
.option("--page-size <n>", `rows per page (1..${MAX_PAGE_SIZE})`, parseBoundedInt(1, MAX_PAGE_SIZE), 25)
|
|
46
47
|
.option("--sort <spec>", "sort: FieldKey-asc | FieldKey-desc, where FieldKey is a record field name, not a " +
|
|
47
|
-
`FilterName (e.g. ${cat.sortKey}-desc)`,
|
|
48
|
+
`FilterName (e.g. ${cat.sortKey}-desc)`, parseSort)
|
|
48
49
|
.option("--filter <spec>", "filter: FilterName~op~'value'~and~… (see `filters`; ops eq|neq|sw|ct|nct|ew|null|nn|gt|lt, " +
|
|
49
50
|
"gt/lt strict; null/nn take ''). A malformed spec or unknown op is rejected. No ~or~: " +
|
|
50
51
|
"for several dropdown codes use one comma list, e.g. Energieträger~eq~'2497,2498'", parseFilter)
|
|
51
|
-
.option("--total", "print only the total match count, not the rows")
|
|
52
|
+
.option("--total", "print only the total match count, not the rows (a one-row request; --page and --page-size are ignored)")
|
|
52
53
|
.action(action(deps, async ({ client, global, opts }) => {
|
|
53
|
-
|
|
54
|
+
// `--total` is the library's count(): a one-row request for the match count.
|
|
55
|
+
const total = opts["total"] === true
|
|
56
|
+
? await client.count(cat.name, buildCountQuery(opts))
|
|
57
|
+
: undefined;
|
|
58
|
+
const page = total === undefined ? await RUN[cat.name](client, buildQuery(opts)) : { total };
|
|
54
59
|
// An unknown --sort key or --filter operator makes the server return 0 rows
|
|
55
60
|
// (not an error), which reads like "no matches". Nudge the user toward the
|
|
56
61
|
// likely cause. Sort keys are the record's field names (`Bruttoleistung`);
|
|
@@ -68,7 +73,7 @@ export function registerCommands(program, deps) {
|
|
|
68
73
|
"dropdown takes its code (the Value from `mastr filters`), not its label, decimals " +
|
|
69
74
|
"take a point ('4999.999'), and gt/lt are strict.");
|
|
70
75
|
}
|
|
71
|
-
renderJson(deps, global,
|
|
76
|
+
renderJson(deps, global, total ?? page);
|
|
72
77
|
}));
|
|
73
78
|
}
|
|
74
79
|
program
|
|
@@ -79,4 +84,3 @@ export function registerCommands(program, deps) {
|
|
|
79
84
|
renderJson(deps, global, await client.filterColumns(category));
|
|
80
85
|
}));
|
|
81
86
|
}
|
|
82
|
-
//# sourceMappingURL=units.js.map
|
package/dist/src/cli/index.d.ts
CHANGED
package/dist/src/cli/index.js
CHANGED
package/dist/src/cli/io.d.ts
CHANGED
package/dist/src/cli/io.js
CHANGED
package/dist/src/cli/program.js
CHANGED
|
@@ -7,6 +7,7 @@ import { Command } from "commander";
|
|
|
7
7
|
import { defaultIO } from "./io.js";
|
|
8
8
|
import { MastrClient } from "../client/client.js";
|
|
9
9
|
import { MAX_TIMEOUT_MS } from "../client/http.js";
|
|
10
|
+
import { MAX_RETRIES } from "../client/engine.js";
|
|
10
11
|
import { parseIntArg, parseBoundedInt, parseHeaderValue, parseBaseUrl } from "./shared.js";
|
|
11
12
|
import { registerCommands } from "./commands/units.js";
|
|
12
13
|
/**
|
|
@@ -44,7 +45,7 @@ export function buildProgram(deps = defaultDeps) {
|
|
|
44
45
|
.option("--base-url <url>", "API base URL (http/https only)", parseBaseUrl, "https://www.marktstammdatenregister.de/MaStR")
|
|
45
46
|
.option("--timeout <ms>", "time limit per request in ms, whole response included (0 = no timeout)", parseBoundedInt(0, MAX_TIMEOUT_MS))
|
|
46
47
|
.option("--user-agent <ua>", "User-Agent header value", parseHeaderValue)
|
|
47
|
-
.option("--max-retries <n>",
|
|
48
|
+
.option("--max-retries <n>", `retries for transient 429/503 responses (0..${MAX_RETRIES}; each waits the server's Retry-After, up to 30 s)`, parseBoundedInt(0, MAX_RETRIES))
|
|
48
49
|
.option("--max-response-bytes <n>", "cap response body size in bytes (0 = unlimited; default 100 MiB)", parseIntArg)
|
|
49
50
|
.option("--compact", "print JSON on a single line instead of pretty-printed")
|
|
50
51
|
.option("--iso-dates", "rewrite MaStR /Date(ms)/ timestamps to ISO-8601")
|
|
@@ -52,4 +53,3 @@ export function buildProgram(deps = defaultDeps) {
|
|
|
52
53
|
registerCommands(program, deps);
|
|
53
54
|
return program;
|
|
54
55
|
}
|
|
55
|
-
//# sourceMappingURL=program.js.map
|
package/dist/src/cli/run.d.ts
CHANGED
package/dist/src/cli/run.js
CHANGED
package/dist/src/cli/shared.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CliDeps } from "./io.js";
|
|
2
2
|
import type { MastrClientOptions } from "../client/client.js";
|
|
3
|
+
import { type Problem } from "../client/validate.js";
|
|
3
4
|
/**
|
|
4
5
|
* commander value-parser: a plain base-10 non-negative integer.
|
|
5
6
|
*
|
|
@@ -8,32 +9,36 @@ import type { MastrClientOptions } from "../client/client.js";
|
|
|
8
9
|
* literals (`0x10`, `0b10`, `1e3`), signs, padding and decimals.
|
|
9
10
|
*/
|
|
10
11
|
export declare function parseIntArg(value: string): number;
|
|
11
|
-
/**
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Build a commander value-parser from a library rule: the rule's reason becomes the
|
|
14
|
+
* usage error, a valid value passes unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseWith(problem: Problem<string>): (value: string) => string;
|
|
17
|
+
/** commander value-parser: a non-empty (after trimming) string (the library's `nonBlankProblem`). */
|
|
18
|
+
export declare const parseNonEmpty: (value: string) => string;
|
|
19
|
+
/** commander value-parser for `--sort`: the library's `sortProblem` (not blank). */
|
|
20
|
+
export declare const parseSort: (value: string) => string;
|
|
13
21
|
/**
|
|
14
22
|
* commander value-parser for `--filter`: non-blank, and not a spec the register would
|
|
15
23
|
* misread (see `filterProblem`), so it becomes a usage error before any request.
|
|
16
24
|
*/
|
|
17
25
|
export declare function parseFilter(value: string): string;
|
|
18
26
|
/**
|
|
19
|
-
* commander value-parser for `--base-url`:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* `data:` / `ftp:`) into a friendly usage error (exit 2) with the value the user
|
|
24
|
-
* actually typed, rather than a network error (exit 6) echoing the fully built
|
|
25
|
-
* request URL. The transport check stays the enforcement point for library callers.
|
|
27
|
+
* commander value-parser for `--base-url`: the library's `baseUrlProblem` (non-blank,
|
|
28
|
+
* no whitespace, an absolute http(s) URL without a query or fragment), whose reason
|
|
29
|
+
* becomes a usage error (exit 2) naming the value the user typed. The CLI keeps no
|
|
30
|
+
* rules of its own; the client constructor enforces the same rule for library callers.
|
|
26
31
|
*/
|
|
27
|
-
export declare
|
|
32
|
+
export declare const parseBaseUrl: (value: string) => string;
|
|
28
33
|
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
29
34
|
export declare function parseBoundedInt(min: number, max: number): (value: string) => number;
|
|
30
35
|
/**
|
|
31
|
-
* commander value-parser for a value that ends up in an HTTP header (User-Agent)
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
36
|
+
* commander value-parser for a value that ends up in an HTTP header (User-Agent):
|
|
37
|
+
* the library's `headerValueProblem` (not blank, Latin-1 without control
|
|
38
|
+
* characters; tab is fine), whose reason becomes the usage error. The engine runs
|
|
39
|
+
* the same rule on `userAgent`.
|
|
35
40
|
*/
|
|
36
|
-
export declare
|
|
41
|
+
export declare const parseHeaderValue: (value: string) => string;
|
|
37
42
|
export interface GlobalOptions {
|
|
38
43
|
baseUrl?: string;
|
|
39
44
|
timeout?: number;
|
|
@@ -74,4 +79,3 @@ export interface ActionContext {
|
|
|
74
79
|
* the trailing options object and command instance to recover the positionals.
|
|
75
80
|
*/
|
|
76
81
|
export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
|
|
77
|
-
//# sourceMappingURL=shared.d.ts.map
|
package/dist/src/cli/shared.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isoifyDates } from "../client/client.js";
|
|
|
5
5
|
import { MastrParseError } from "../client/errors.js";
|
|
6
6
|
import { filterProblem } from "../client/filter.js";
|
|
7
7
|
import { isBidiControl } from "../client/engine.js";
|
|
8
|
+
import { baseUrlProblem, headerValueProblem, nonBlankProblem, sortProblem, } from "../client/validate.js";
|
|
8
9
|
/**
|
|
9
10
|
* commander value-parser: a plain base-10 non-negative integer.
|
|
10
11
|
*
|
|
@@ -22,13 +23,22 @@ export function parseIntArg(value) {
|
|
|
22
23
|
}
|
|
23
24
|
return n;
|
|
24
25
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return value
|
|
26
|
+
/**
|
|
27
|
+
* Build a commander value-parser from a library rule: the rule's reason becomes the
|
|
28
|
+
* usage error, a valid value passes unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export function parseWith(problem) {
|
|
31
|
+
return (value) => {
|
|
32
|
+
const reason = problem(value);
|
|
33
|
+
if (reason !== undefined)
|
|
34
|
+
throw new InvalidArgumentError(reason);
|
|
35
|
+
return value;
|
|
36
|
+
};
|
|
31
37
|
}
|
|
38
|
+
/** commander value-parser: a non-empty (after trimming) string (the library's `nonBlankProblem`). */
|
|
39
|
+
export const parseNonEmpty = parseWith(nonBlankProblem);
|
|
40
|
+
/** commander value-parser for `--sort`: the library's `sortProblem` (not blank). */
|
|
41
|
+
export const parseSort = parseWith(sortProblem);
|
|
32
42
|
/**
|
|
33
43
|
* commander value-parser for `--filter`: non-blank, and not a spec the register would
|
|
34
44
|
* misread (see `filterProblem`), so it becomes a usage error before any request.
|
|
@@ -41,41 +51,12 @@ export function parseFilter(value) {
|
|
|
41
51
|
return value;
|
|
42
52
|
}
|
|
43
53
|
/**
|
|
44
|
-
* commander value-parser for `--base-url`:
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `data:` / `ftp:`) into a friendly usage error (exit 2) with the value the user
|
|
49
|
-
* actually typed, rather than a network error (exit 6) echoing the fully built
|
|
50
|
-
* request URL. The transport check stays the enforcement point for library callers.
|
|
54
|
+
* commander value-parser for `--base-url`: the library's `baseUrlProblem` (non-blank,
|
|
55
|
+
* no whitespace, an absolute http(s) URL without a query or fragment), whose reason
|
|
56
|
+
* becomes a usage error (exit 2) naming the value the user typed. The CLI keeps no
|
|
57
|
+
* rules of its own; the client constructor enforces the same rule for library callers.
|
|
51
58
|
*/
|
|
52
|
-
export
|
|
53
|
-
const trimmed = value.trim();
|
|
54
|
-
if (trimmed === "") {
|
|
55
|
-
throw new InvalidArgumentError("Expected a non-empty value.");
|
|
56
|
-
}
|
|
57
|
-
let parsed;
|
|
58
|
-
try {
|
|
59
|
-
parsed = new URL(trimmed);
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
throw new InvalidArgumentError("Expected an absolute http(s) URL.");
|
|
63
|
-
}
|
|
64
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
65
|
-
throw new InvalidArgumentError("Only http and https URLs are supported.");
|
|
66
|
-
}
|
|
67
|
-
// Paths are appended to the base URL as a string, so a query or fragment would
|
|
68
|
-
// swallow every request path ("http://h/#f" requests "/" for every command).
|
|
69
|
-
if (/[?#]/.test(value)) {
|
|
70
|
-
throw new InvalidArgumentError("A base URL cannot have a query (?) or fragment (#).");
|
|
71
|
-
}
|
|
72
|
-
// new URL() trims surrounding whitespace silently; the raw value is what the
|
|
73
|
-
// engine uses, so reject it rather than guess.
|
|
74
|
-
if (value !== value.trim()) {
|
|
75
|
-
throw new InvalidArgumentError("A base URL cannot have surrounding whitespace.");
|
|
76
|
-
}
|
|
77
|
-
return value;
|
|
78
|
-
}
|
|
59
|
+
export const parseBaseUrl = parseWith(baseUrlProblem);
|
|
79
60
|
/** Build a commander value-parser for an integer constrained to [min, max]. */
|
|
80
61
|
export function parseBoundedInt(min, max) {
|
|
81
62
|
return (value) => {
|
|
@@ -88,20 +69,12 @@ export function parseBoundedInt(min, max) {
|
|
|
88
69
|
};
|
|
89
70
|
}
|
|
90
71
|
/**
|
|
91
|
-
* commander value-parser for a value that ends up in an HTTP header (User-Agent)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
72
|
+
* commander value-parser for a value that ends up in an HTTP header (User-Agent):
|
|
73
|
+
* the library's `headerValueProblem` (not blank, Latin-1 without control
|
|
74
|
+
* characters; tab is fine), whose reason becomes the usage error. The engine runs
|
|
75
|
+
* the same rule on `userAgent`.
|
|
95
76
|
*/
|
|
96
|
-
export
|
|
97
|
-
for (let i = 0; i < value.length; i++) {
|
|
98
|
-
const c = value.charCodeAt(i);
|
|
99
|
-
if ((c < 0x20 && c !== 0x09) || c === 0x7f) {
|
|
100
|
-
throw new InvalidArgumentError("Value contains control characters.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return value;
|
|
104
|
-
}
|
|
77
|
+
export const parseHeaderValue = parseWith(headerValueProblem);
|
|
105
78
|
/** Translate resolved global CLI options into client options. */
|
|
106
79
|
export function toEngineOptions(global) {
|
|
107
80
|
const options = {};
|
|
@@ -176,4 +149,3 @@ export function action(deps, fn) {
|
|
|
176
149
|
await fn({ client, global, opts: command.opts() }, positionals);
|
|
177
150
|
};
|
|
178
151
|
}
|
|
179
|
-
//# sourceMappingURL=shared.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EngineOptions } from "./engine.js";
|
|
2
|
-
import type { FilterColumn, UnitCategory, UnitPage, UnitQuery } from "./types.js";
|
|
2
|
+
import type { CountQuery, FilterColumn, UnitCategory, UnitPage, UnitQuery } from "./types.js";
|
|
3
3
|
/** Largest `page` the client (and the CLI's `--page`) accepts. */
|
|
4
4
|
export declare const MAX_PAGE = 1000000;
|
|
5
5
|
/** Largest `pageSize` the client (and the CLI's `--page-size`) accepts. */
|
|
@@ -26,10 +26,20 @@ export declare class MastrClient {
|
|
|
26
26
|
* `sort`, `page`, `pageSize`, `group`, `filter` — always, because the server
|
|
27
27
|
* rejects a request with `group`/`filter` missing ("Die Anfrage ist Null.").
|
|
28
28
|
* An unknown category, a `page` outside 1..`MAX_PAGE`, a `pageSize` outside
|
|
29
|
-
* 1..`MAX_PAGE_SIZE` or a filter the register
|
|
30
|
-
* {@link validateFilter}) is rejected with a
|
|
29
|
+
* 1..`MAX_PAGE_SIZE`, a blank `sort` ({@link sortProblem}) or a filter the register
|
|
30
|
+
* would misread (e.g. `~or~`, see {@link validateFilter}) is rejected with a
|
|
31
|
+
* `MastrValidationError` before any request.
|
|
31
32
|
*/
|
|
32
33
|
units(category: UnitCategory, query?: UnitQuery): Promise<UnitPage>;
|
|
34
|
+
/**
|
|
35
|
+
* The number of units in a category that match `filter` (all units without one).
|
|
36
|
+
* Fetches a single row (`page=1`, `pageSize=1`) and returns the envelope's
|
|
37
|
+
* `Total`, which counts every match regardless of the page. `sort` is forwarded:
|
|
38
|
+
* an unknown sort key makes the register answer 0. Paging options are refused
|
|
39
|
+
* (`countQueryProblem`), as are the inputs `units()` refuses, all with a
|
|
40
|
+
* `MastrValidationError` before any request.
|
|
41
|
+
*/
|
|
42
|
+
count(category: UnitCategory, query?: CountQuery): Promise<number>;
|
|
33
43
|
/** Electricity-generation units (`Stromerzeugung`). */
|
|
34
44
|
stromerzeugung(query?: UnitQuery): Promise<UnitPage>;
|
|
35
45
|
/** Electricity-consumption units (`Stromverbrauch`). */
|
|
@@ -45,4 +55,3 @@ export declare class MastrClient {
|
|
|
45
55
|
*/
|
|
46
56
|
filterColumns(category: UnitCategory): Promise<FilterColumn[]>;
|
|
47
57
|
}
|
|
48
|
-
//# sourceMappingURL=client.d.ts.map
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { RequestEngine, describeMastrErrors } from "./engine.js";
|
|
13
13
|
import { MastrApiError, MastrParseError, MastrValidationError } from "./errors.js";
|
|
14
14
|
import { validateFilter } from "./filter.js";
|
|
15
|
+
import { assertValid, countQueryProblem, sortProblem } from "./validate.js";
|
|
15
16
|
const SERVICE = "/Einheit/EinheitJson";
|
|
16
17
|
/** Map a category to the PascalCase suffix used in the endpoint names. */
|
|
17
18
|
const CATEGORY_SUFFIX = {
|
|
@@ -98,13 +99,16 @@ export class MastrClient {
|
|
|
98
99
|
* `sort`, `page`, `pageSize`, `group`, `filter` — always, because the server
|
|
99
100
|
* rejects a request with `group`/`filter` missing ("Die Anfrage ist Null.").
|
|
100
101
|
* An unknown category, a `page` outside 1..`MAX_PAGE`, a `pageSize` outside
|
|
101
|
-
* 1..`MAX_PAGE_SIZE` or a filter the register
|
|
102
|
-
* {@link validateFilter}) is rejected with a
|
|
102
|
+
* 1..`MAX_PAGE_SIZE`, a blank `sort` ({@link sortProblem}) or a filter the register
|
|
103
|
+
* would misread (e.g. `~or~`, see {@link validateFilter}) is rejected with a
|
|
104
|
+
* `MastrValidationError` before any request.
|
|
103
105
|
*/
|
|
104
106
|
async units(category, query = {}) {
|
|
105
107
|
const suffix = categorySuffix(category);
|
|
106
108
|
checkPaging("page", query.page, MAX_PAGE);
|
|
107
109
|
checkPaging("pageSize", query.pageSize, MAX_PAGE_SIZE);
|
|
110
|
+
if (query.sort !== undefined)
|
|
111
|
+
assertValid("sort", query.sort, sortProblem);
|
|
108
112
|
if (query.filter !== undefined)
|
|
109
113
|
validateFilter(query.filter);
|
|
110
114
|
const params = {
|
|
@@ -142,6 +146,23 @@ export class MastrClient {
|
|
|
142
146
|
throw shapeError(path, "a Data array");
|
|
143
147
|
return { total, data: data };
|
|
144
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* The number of units in a category that match `filter` (all units without one).
|
|
151
|
+
* Fetches a single row (`page=1`, `pageSize=1`) and returns the envelope's
|
|
152
|
+
* `Total`, which counts every match regardless of the page. `sort` is forwarded:
|
|
153
|
+
* an unknown sort key makes the register answer 0. Paging options are refused
|
|
154
|
+
* (`countQueryProblem`), as are the inputs `units()` refuses, all with a
|
|
155
|
+
* `MastrValidationError` before any request.
|
|
156
|
+
*/
|
|
157
|
+
async count(category, query = {}) {
|
|
158
|
+
assertValid("count query", query, countQueryProblem);
|
|
159
|
+
const q = { page: 1, pageSize: 1 };
|
|
160
|
+
if (query.sort !== undefined)
|
|
161
|
+
q.sort = query.sort;
|
|
162
|
+
if (query.filter !== undefined)
|
|
163
|
+
q.filter = query.filter;
|
|
164
|
+
return (await this.units(category, q)).total;
|
|
165
|
+
}
|
|
145
166
|
/** Electricity-generation units (`Stromerzeugung`). */
|
|
146
167
|
stromerzeugung(query) {
|
|
147
168
|
return this.units("stromerzeugung", query);
|
|
@@ -180,4 +201,3 @@ export class MastrClient {
|
|
|
180
201
|
return res;
|
|
181
202
|
}
|
|
182
203
|
}
|
|
183
|
-
//# sourceMappingURL=client.js.map
|
|
@@ -7,35 +7,51 @@ export interface RawResponse {
|
|
|
7
7
|
status: number;
|
|
8
8
|
}
|
|
9
9
|
export interface EngineOptions {
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Base URL of the API. Defaults to the canonical marktstammdatenregister.de base.
|
|
12
|
+
* A value that breaks a rule of {@link validateBaseUrl} (blank, whitespace or
|
|
13
|
+
* control characters, not an absolute http(s) URL, a query or fragment) throws a
|
|
14
|
+
* MastrValidationError.
|
|
15
|
+
*/
|
|
11
16
|
baseUrl?: string;
|
|
12
17
|
/** Swappable transport. Defaults to the built-in node http/https transport. */
|
|
13
18
|
transport?: Transport;
|
|
14
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Value of the User-Agent header: not blank, Latin-1 without control characters
|
|
21
|
+
* (tab is fine), else a MastrValidationError.
|
|
22
|
+
*/
|
|
15
23
|
userAgent?: string;
|
|
16
|
-
/** Extra headers sent on every request. */
|
|
24
|
+
/** Extra headers sent on every request; names must be tokens, values follow the `userAgent` rule. */
|
|
17
25
|
defaultHeaders?: Record<string, string>;
|
|
18
26
|
/**
|
|
19
27
|
* Time limit per request in milliseconds, covering the whole response body, not
|
|
20
|
-
* only idle gaps
|
|
28
|
+
* only idle gaps: an integer 0..`MAX_TIMEOUT_MS` (2^31 - 1 ms); 0 disables.
|
|
29
|
+
* Defaults to 30000.
|
|
21
30
|
*/
|
|
22
31
|
timeoutMs?: number;
|
|
23
32
|
/**
|
|
24
|
-
* Number of automatic retries for transient (429/503) responses
|
|
25
|
-
*
|
|
26
|
-
* retried), or else
|
|
33
|
+
* Number of automatic retries for transient (429/503) responses, an integer
|
|
34
|
+
* 0..`MAX_RETRIES` (10); defaults to 2. Each waits the response's `Retry-After`
|
|
35
|
+
* (up to `MAX_RETRY_AFTER_MS`; a longer one is not retried), or else
|
|
36
|
+
* `retryDelayMs * attempt`.
|
|
27
37
|
*/
|
|
28
38
|
maxRetries?: number;
|
|
29
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Base backoff between retries in milliseconds (grows linearly), a non-negative
|
|
41
|
+
* integer; used without a Retry-After. Defaults to 200.
|
|
42
|
+
*/
|
|
30
43
|
retryDelayMs?: number;
|
|
31
44
|
/**
|
|
32
45
|
* Hard cap on response body size in bytes (defends against memory exhaustion
|
|
33
|
-
* from a hostile/buggy endpoint). Defaults to 100 MiB;
|
|
46
|
+
* from a hostile/buggy endpoint), a non-negative integer. Defaults to 100 MiB;
|
|
47
|
+
* set to 0 for no limit.
|
|
34
48
|
*/
|
|
35
49
|
maxResponseBytes?: number;
|
|
36
50
|
/** Injectable sleep, primarily for deterministic tests. */
|
|
37
51
|
sleep?: (ms: number) => Promise<void>;
|
|
38
52
|
}
|
|
53
|
+
/** Most retries `maxRetries` may ask for (each may wait up to `MAX_RETRY_AFTER_MS`). */
|
|
54
|
+
export declare const MAX_RETRIES = 10;
|
|
39
55
|
/**
|
|
40
56
|
* Longest `Retry-After` the engine waits out before retrying a 429/503. When the
|
|
41
57
|
* server asks for longer, the engine does not retry at all and surfaces the error at
|
|
@@ -87,6 +103,22 @@ export declare function sanitizeServerText(text: string): string;
|
|
|
87
103
|
* Returns `undefined` when nothing readable is left.
|
|
88
104
|
*/
|
|
89
105
|
export declare function describeMastrErrors(errors: unknown): string | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Check a base URL against every rule of {@link baseUrlProblem} — blank, whitespace
|
|
108
|
+
* or control characters, not an absolute URL, a scheme other than `http:`/`https:`,
|
|
109
|
+
* a query or fragment — and return it with trailing slashes stripped. A bad value
|
|
110
|
+
* throws a MastrValidationError (`Invalid baseUrl: <reason>`): it is a configuration
|
|
111
|
+
* error, not a transport failure. The default transport still gates the scheme per
|
|
112
|
+
* request, but the engine may be handed a custom transport that does no such check,
|
|
113
|
+
* so the configured value is checked here, on the raw string.
|
|
114
|
+
*/
|
|
115
|
+
export declare function validateBaseUrl(raw: string): string;
|
|
116
|
+
/**
|
|
117
|
+
* Check a value bound for an HTTP header (`headerValueProblem`) and return it, or
|
|
118
|
+
* throw a MastrValidationError (`Invalid <name>: <reason>`). The engine runs it on
|
|
119
|
+
* `userAgent` and every `defaultHeaders` value before any request.
|
|
120
|
+
*/
|
|
121
|
+
export declare function assertHeaderValue(name: string, value: string): string;
|
|
90
122
|
export declare class RequestEngine {
|
|
91
123
|
private readonly baseUrl;
|
|
92
124
|
private readonly transport;
|
|
@@ -114,4 +146,3 @@ export declare class RequestEngine {
|
|
|
114
146
|
getJson<T>(path: string, query?: QueryParams): Promise<T>;
|
|
115
147
|
private toApiError;
|
|
116
148
|
}
|
|
117
|
-
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -2,12 +2,22 @@
|
|
|
2
2
|
// a Transport, applies retry/backoff for transient statuses (429, 503), and decodes
|
|
3
3
|
// JSON responses. MaStR's public search backend is an unauthenticated GET API whose
|
|
4
4
|
// parameters travel in the query string.
|
|
5
|
-
import { nodeHttpTransport } from "./http.js";
|
|
5
|
+
import { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
|
|
6
6
|
import { buildQueryString } from "./query.js";
|
|
7
|
-
import { MastrApiError,
|
|
7
|
+
import { MastrApiError, MastrParseError } from "./errors.js";
|
|
8
|
+
import { assertValid, baseUrlProblem, headerNameProblem, headerValueProblem, intRangeProblem, } from "./validate.js";
|
|
8
9
|
export const DEFAULT_BASE_URL = "https://www.marktstammdatenregister.de/MaStR";
|
|
9
10
|
const DEFAULT_USER_AGENT = "marktstammdatenregister-cli";
|
|
10
11
|
const DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
12
|
+
/** Most retries `maxRetries` may ask for (each may wait up to `MAX_RETRY_AFTER_MS`). */
|
|
13
|
+
export const MAX_RETRIES = 10;
|
|
14
|
+
/**
|
|
15
|
+
* A numeric engine option: `fallback` when undefined, else an integer in 0..max,
|
|
16
|
+
* or a MastrValidationError (`Invalid <name>: expected an integer …`).
|
|
17
|
+
*/
|
|
18
|
+
function intOption(name, value, max, fallback) {
|
|
19
|
+
return value === undefined ? fallback : assertValid(name, value, intRangeProblem(0, max));
|
|
20
|
+
}
|
|
11
21
|
/**
|
|
12
22
|
* Longest `Retry-After` the engine waits out before retrying a 429/503. When the
|
|
13
23
|
* server asks for longer, the engine does not retry at all and surfaces the error at
|
|
@@ -103,28 +113,33 @@ export function describeMastrErrors(errors) {
|
|
|
103
113
|
return found.length > 0 ? found.join("; ") : undefined;
|
|
104
114
|
}
|
|
105
115
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
116
|
+
* Check a base URL against every rule of {@link baseUrlProblem} — blank, whitespace
|
|
117
|
+
* or control characters, not an absolute URL, a scheme other than `http:`/`https:`,
|
|
118
|
+
* a query or fragment — and return it with trailing slashes stripped. A bad value
|
|
119
|
+
* throws a MastrValidationError (`Invalid baseUrl: <reason>`): it is a configuration
|
|
120
|
+
* error, not a transport failure. The default transport still gates the scheme per
|
|
121
|
+
* request, but the engine may be handed a custom transport that does no such check,
|
|
122
|
+
* so the configured value is checked here, on the raw string.
|
|
113
123
|
*/
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
export function validateBaseUrl(raw) {
|
|
125
|
+
return assertValid("baseUrl", raw, baseUrlProblem).replace(/\/+$/, "");
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Check a value bound for an HTTP header (`headerValueProblem`) and return it, or
|
|
129
|
+
* throw a MastrValidationError (`Invalid <name>: <reason>`). The engine runs it on
|
|
130
|
+
* `userAgent` and every `defaultHeaders` value before any request.
|
|
131
|
+
*/
|
|
132
|
+
export function assertHeaderValue(name, value) {
|
|
133
|
+
return assertValid(name, value, headerValueProblem);
|
|
134
|
+
}
|
|
135
|
+
/** Check every `defaultHeaders` name (a token) and value; returns a copy. */
|
|
136
|
+
function checkedHeaders(headers) {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
139
|
+
assertValid("defaultHeaders name", name, headerNameProblem);
|
|
140
|
+
out[name] = assertHeaderValue(`defaultHeaders["${name}"]`, value);
|
|
127
141
|
}
|
|
142
|
+
return out;
|
|
128
143
|
}
|
|
129
144
|
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
145
|
export class RequestEngine {
|
|
@@ -138,15 +153,24 @@ export class RequestEngine {
|
|
|
138
153
|
maxResponseBytes;
|
|
139
154
|
sleep;
|
|
140
155
|
constructor(options = {}) {
|
|
141
|
-
|
|
142
|
-
|
|
156
|
+
// The raw value is checked before the trailing-slash strip, so "https://h/ "
|
|
157
|
+
// cannot slip past it; only an omitted baseUrl selects the default.
|
|
158
|
+
this.baseUrl = validateBaseUrl(options.baseUrl === undefined ? DEFAULT_BASE_URL : options.baseUrl);
|
|
143
159
|
this.transport = options.transport ?? nodeHttpTransport;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
|
|
160
|
+
// Header values are checked up front: a blank one would be sent as is, and a
|
|
161
|
+
// CR/LF or a character above U+00FF would reach a custom transport raw or make
|
|
162
|
+
// Node's HTTP layer throw an untyped ERR_INVALID_CHAR. Only an omitted
|
|
163
|
+
// userAgent selects the default.
|
|
164
|
+
this.userAgent =
|
|
165
|
+
options.userAgent === undefined ? DEFAULT_USER_AGENT : assertHeaderValue("userAgent", options.userAgent);
|
|
166
|
+
this.defaultHeaders = checkedHeaders(options.defaultHeaders ?? {});
|
|
167
|
+
// Range-check the numeric options: a negative, NaN or fractional value would
|
|
168
|
+
// otherwise silently disable the timeout or the size cap, and an unbounded
|
|
169
|
+
// maxRetries would keep retrying against the production register.
|
|
170
|
+
this.timeoutMs = intOption("timeoutMs", options.timeoutMs, MAX_TIMEOUT_MS, 30_000);
|
|
171
|
+
this.maxRetries = intOption("maxRetries", options.maxRetries, MAX_RETRIES, 2);
|
|
172
|
+
this.retryDelayMs = intOption("retryDelayMs", options.retryDelayMs, Number.MAX_SAFE_INTEGER, 200);
|
|
173
|
+
this.maxResponseBytes = intOption("maxResponseBytes", options.maxResponseBytes, Number.MAX_SAFE_INTEGER, DEFAULT_MAX_RESPONSE_BYTES);
|
|
150
174
|
this.sleep = options.sleep ?? realSleep;
|
|
151
175
|
}
|
|
152
176
|
/** Build a fully-qualified URL from a path and optional query parameters. */
|
|
@@ -244,4 +268,3 @@ export class RequestEngine {
|
|
|
244
268
|
return new MastrApiError({ status, url, method: "GET", body: text, detail });
|
|
245
269
|
}
|
|
246
270
|
}
|
|
247
|
-
//# sourceMappingURL=engine.js.map
|