@maschinenlesbar.org/marktstammdatenregister-cli 0.0.7 → 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.
Files changed (62) hide show
  1. package/README.md +24 -4
  2. package/dist/src/cli/commands/units.d.ts +0 -1
  3. package/dist/src/cli/commands/units.js +38 -27
  4. package/dist/src/cli/index.d.ts +0 -1
  5. package/dist/src/cli/index.js +0 -1
  6. package/dist/src/cli/io.d.ts +0 -1
  7. package/dist/src/cli/io.js +0 -1
  8. package/dist/src/cli/program.d.ts +0 -1
  9. package/dist/src/cli/program.js +2 -2
  10. package/dist/src/cli/run.d.ts +0 -1
  11. package/dist/src/cli/run.js +0 -1
  12. package/dist/src/cli/shared.d.ts +29 -19
  13. package/dist/src/cli/shared.js +42 -46
  14. package/dist/src/client/client.d.ts +26 -4
  15. package/dist/src/client/client.js +105 -19
  16. package/dist/src/client/engine.d.ts +96 -18
  17. package/dist/src/client/engine.js +154 -43
  18. package/dist/src/client/errors.d.ts +12 -2
  19. package/dist/src/client/errors.js +29 -4
  20. package/dist/src/client/filter.d.ts +56 -0
  21. package/dist/src/client/filter.js +144 -0
  22. package/dist/src/client/http.d.ts +0 -1
  23. package/dist/src/client/http.js +45 -35
  24. package/dist/src/client/index.d.ts +7 -4
  25. package/dist/src/client/index.js +5 -4
  26. package/dist/src/client/query.d.ts +0 -1
  27. package/dist/src/client/query.js +0 -1
  28. package/dist/src/client/types.d.ts +29 -12
  29. package/dist/src/client/types.js +0 -1
  30. package/dist/src/client/validate.d.ts +63 -0
  31. package/dist/src/client/validate.js +131 -0
  32. package/dist/src/index.d.ts +0 -1
  33. package/dist/src/index.js +0 -1
  34. package/package.json +2 -1
  35. package/dist/src/cli/commands/units.d.ts.map +0 -1
  36. package/dist/src/cli/commands/units.js.map +0 -1
  37. package/dist/src/cli/index.d.ts.map +0 -1
  38. package/dist/src/cli/index.js.map +0 -1
  39. package/dist/src/cli/io.d.ts.map +0 -1
  40. package/dist/src/cli/io.js.map +0 -1
  41. package/dist/src/cli/program.d.ts.map +0 -1
  42. package/dist/src/cli/program.js.map +0 -1
  43. package/dist/src/cli/run.d.ts.map +0 -1
  44. package/dist/src/cli/run.js.map +0 -1
  45. package/dist/src/cli/shared.d.ts.map +0 -1
  46. package/dist/src/cli/shared.js.map +0 -1
  47. package/dist/src/client/client.d.ts.map +0 -1
  48. package/dist/src/client/client.js.map +0 -1
  49. package/dist/src/client/engine.d.ts.map +0 -1
  50. package/dist/src/client/engine.js.map +0 -1
  51. package/dist/src/client/errors.d.ts.map +0 -1
  52. package/dist/src/client/errors.js.map +0 -1
  53. package/dist/src/client/http.d.ts.map +0 -1
  54. package/dist/src/client/http.js.map +0 -1
  55. package/dist/src/client/index.d.ts.map +0 -1
  56. package/dist/src/client/index.js.map +0 -1
  57. package/dist/src/client/query.d.ts.map +0 -1
  58. package/dist/src/client/query.js.map +0 -1
  59. package/dist/src/client/types.d.ts.map +0 -1
  60. package/dist/src/client/types.js.map +0 -1
  61. package/dist/src/index.d.ts.map +0 -1
  62. package/dist/src/index.js.map +0 -1
package/README.md CHANGED
@@ -42,11 +42,15 @@ mastr filters stromerzeugung # the filterable columns + d
42
42
 
43
43
  Categories: `stromerzeugung`, `stromverbrauch`, `gaserzeugung`, `gasverbrauch`.
44
44
 
45
- **Filtering** uses the register's own syntax — `FilterName~op~'value'~[and|or]~…`
45
+ **Filtering** uses the register's own syntax — `FilterName~op~'value'~and~…`
46
46
  (ops `eq|neq|sw|ct|nct|ew|null|nn`, plus the strict `gt`/`lt` for numbers and dates);
47
- discover the German field names and dropdown codes with `mastr filters <category>`. A
48
- **wrong `FilterName` is silently ignored** (you get the unfiltered set), while a **wrong
49
- operator** (e.g. `gte`) or a **wrong `--sort` key returns 0 rows**. Verify filter names
47
+ there is **no working `~or~`** (the register drops everything after it, so the CLI
48
+ rejects it) — for several codes of one dropdown, list them in one value:
49
+ `Energieträger~eq~'2497,2498'`. Discover the German field names and dropdown codes with `mastr filters <category>`. A
50
+ **wrong `FilterName` is silently ignored** (you get the unfiltered set), and a **wrong
51
+ `--sort` key returns 0 rows**; a malformed spec or an unknown operator (e.g. `gte`) is
52
+ rejected before any request (exit 2), because the register would answer it with a wrong
53
+ count. Verify filter names
50
54
  with `mastr filters` and sanity-check with `--total`; sort keys are record field names
51
55
  (`Bruttoleistung`), not FilterNames — list them with
52
56
  `mastr stromerzeugung --page-size 1 --compact | jq '.data[0] | keys'`. **Dates** come as Microsoft
@@ -63,9 +67,25 @@ import { MastrClient, parseMsDate } from "@maschinenlesbar.org/marktstammdatenre
63
67
  const mastr = new MastrClient();
64
68
  const solar = await mastr.stromerzeugung({ filter: "Energieträger~eq~'2495'", pageSize: 10 });
65
69
  solar.total; // total matching units
70
+ await mastr.count("stromerzeugung", { filter: "Energieträger~eq~'2495'" }); // just the count, one-row request
66
71
  parseMsDate(String(solar.data[0]?.EinheitRegistrierungsdatum)); // Date | null
67
72
  ```
68
73
 
74
+ Build filters from user input with `buildFilter`, not string interpolation: a filter
75
+ value cannot contain `~` (the register splits on it and has no escape), so
76
+ `` `Ort~eq~'${input}'` `` lets an input like `Münster'~and~Energieträger~eq~'2497` add a
77
+ condition. `buildFilter` quotes each value and throws `MastrValidationError` for a `~`:
78
+
79
+ ```ts
80
+ import { buildFilter } from "@maschinenlesbar.org/marktstammdatenregister-cli";
81
+
82
+ const filter = buildFilter([
83
+ { name: "Ort", op: "eq", value: town }, // quoted, ~ refused
84
+ { name: "Energieträger", op: "eq", value: ["2497", "2498"] }, // comma list = any of
85
+ ]);
86
+ await mastr.stromerzeugung({ filter, pageSize: 1 });
87
+ ```
88
+
69
89
  ## Documentation
70
90
 
71
91
  - [Usage.md](Usage.md) — commands, filter syntax, exit codes
@@ -1,4 +1,3 @@
1
1
  import { type Command } from "commander";
2
2
  import type { CliDeps } from "../io.js";
3
3
  export declare function registerCommands(program: Command, deps: CliDeps): void;
4
- //# sourceMappingURL=units.d.ts.map
@@ -2,12 +2,16 @@
2
2
  // gas × generation / consumption), each a paged, optionally-filtered search, plus a
3
3
  // `filters` command to discover the filterable columns and their dropdown codes.
4
4
  import { Argument } from "commander";
5
- import { action, parseBoundedInt, parseNonEmpty, renderJson } from "../shared.js";
5
+ import { MAX_PAGE, MAX_PAGE_SIZE } from "../../client/client.js";
6
+ import { action, parseBoundedInt, parseFilter, parseSort, renderJson } from "../shared.js";
7
+ // `sortKey` is a record field that exists in that category's rows (live 2026-09-26):
8
+ // only stromerzeugung rows carry Bruttoleistung/Nettonennleistung, so the stderr hint
9
+ // must not suggest it for the other three.
6
10
  const CATEGORIES = [
7
- { name: "stromerzeugung", desc: "Electricity-generation units (Stromerzeugung)" },
8
- { name: "stromverbrauch", desc: "Electricity-consumption units (Stromverbrauch)" },
9
- { name: "gaserzeugung", desc: "Gas-generation units (Gaserzeugung)" },
10
- { name: "gasverbrauch", desc: "Gas-consumption units (Gasverbrauch)" },
11
+ { name: "stromerzeugung", desc: "Electricity-generation units (Stromerzeugung)", sortKey: "Bruttoleistung" },
12
+ { name: "stromverbrauch", desc: "Electricity-consumption units (Stromverbrauch)", sortKey: "InbetriebnahmeDatum" },
13
+ { name: "gaserzeugung", desc: "Gas-generation units (Gaserzeugung)", sortKey: "Erzeugungsleistung" },
14
+ { name: "gasverbrauch", desc: "Gas-consumption units (Gasverbrauch)", sortKey: "MaximaleGasbezugsLeistung" },
11
15
  ];
12
16
  const RUN = {
13
17
  stromerzeugung: (c, q) => c.stromerzeugung(q),
@@ -15,21 +19,22 @@ const RUN = {
15
19
  gaserzeugung: (c, q) => c.gaserzeugung(q),
16
20
  gasverbrauch: (c, q) => c.gasverbrauch(q),
17
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
+ }
18
31
  /** Build a UnitQuery from this command's parsed options. */
19
32
  function buildQuery(opts) {
20
- const q = {};
33
+ const q = buildCountQuery(opts);
21
34
  if (typeof opts["page"] === "number")
22
35
  q.page = opts["page"];
23
36
  if (typeof opts["pageSize"] === "number")
24
37
  q.pageSize = opts["pageSize"];
25
- if (typeof opts["sort"] === "string")
26
- q.sort = opts["sort"];
27
- if (typeof opts["filter"] === "string")
28
- q.filter = opts["filter"];
29
- // `--total` needs only the match count (returned regardless of page size), so
30
- // request a single row instead of fetching and discarding a full page.
31
- if (opts["total"] === true)
32
- q.pageSize = 1;
33
38
  return q;
34
39
  }
35
40
  export function registerCommands(program, deps) {
@@ -37,31 +42,38 @@ export function registerCommands(program, deps) {
37
42
  program
38
43
  .command(cat.name)
39
44
  .description(cat.desc)
40
- .option("--page <n>", "1-based page number", parseBoundedInt(1, 1_000_000), 1)
41
- .option("--page-size <n>", "rows per page (1..5000)", parseBoundedInt(1, 5000), 25)
45
+ .option("--page <n>", "1-based page number", parseBoundedInt(1, MAX_PAGE), 1)
46
+ .option("--page-size <n>", `rows per page (1..${MAX_PAGE_SIZE})`, parseBoundedInt(1, MAX_PAGE_SIZE), 25)
42
47
  .option("--sort <spec>", "sort: FieldKey-asc | FieldKey-desc, where FieldKey is a record field name, not a " +
43
- "FilterName (e.g. Bruttoleistung-desc)", parseNonEmpty)
44
- .option("--filter <spec>", "filter: FilterName~op~'value'~[and|or]~… (see `filters`; ops eq|neq|sw|ct|nct|ew|null|nn|gt|lt, " +
45
- "gt/lt strict; an unknown op returns 0 rows)", parseNonEmpty)
46
- .option("--total", "print only the total match count, not the rows")
48
+ `FilterName (e.g. ${cat.sortKey}-desc)`, parseSort)
49
+ .option("--filter <spec>", "filter: FilterName~op~'value'~and~… (see `filters`; ops eq|neq|sw|ct|nct|ew|null|nn|gt|lt, " +
50
+ "gt/lt strict; null/nn take ''). A malformed spec or unknown op is rejected. No ~or~: " +
51
+ "for several dropdown codes use one comma list, e.g. Energieträger~eq~'2497,2498'", parseFilter)
52
+ .option("--total", "print only the total match count, not the rows (a one-row request; --page and --page-size are ignored)")
47
53
  .action(action(deps, async ({ client, global, opts }) => {
48
- const page = await RUN[cat.name](client, buildQuery(opts));
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 };
49
59
  // An unknown --sort key or --filter operator makes the server return 0 rows
50
60
  // (not an error), which reads like "no matches". Nudge the user toward the
51
61
  // likely cause. Sort keys are the record's field names (`Bruttoleistung`);
52
62
  // the FilterNames from `mastr filters` ("Bruttoleistung der Einheit") don't sort.
53
63
  if (page.total === 0 && typeof opts["sort"] === "string") {
54
64
  deps.io.err("Note: 0 results with --sort set. If you expected matches, an unknown sort " +
55
- "key returns 0 rows. Sort keys are record field names (e.g. Bruttoleistung), " +
65
+ `key returns 0 rows. Sort keys are record field names (e.g. ${cat.sortKey}), ` +
56
66
  "not the FilterNames from `mastr filters`; list them with " +
57
67
  `\`mastr ${cat.name} --page-size 1 --compact | jq '.data[0] | keys'\`.`);
58
68
  }
69
+ // An unknown operator is already a usage error (filterProblem); what is left
70
+ // that silently gives 0 rows is a value the register can't match.
59
71
  if (page.total === 0 && typeof opts["filter"] === "string") {
60
- deps.io.err("Note: 0 results with --filter set. If you expected matches, check the operators: " +
61
- "the known ones are eq, neq, sw, ct, nct, ew, null, nn, gt and lt; an unknown " +
62
- "operator (e.g. gte) returns 0 rows.");
72
+ deps.io.err("Note: 0 results with --filter set. If you expected matches, check the values: a " +
73
+ "dropdown takes its code (the Value from `mastr filters`), not its label, decimals " +
74
+ "take a point ('4999.999'), and gt/lt are strict.");
63
75
  }
64
- renderJson(deps, global, opts["total"] === true ? page.total : page);
76
+ renderJson(deps, global, total ?? page);
65
77
  }));
66
78
  }
67
79
  program
@@ -72,4 +84,3 @@ export function registerCommands(program, deps) {
72
84
  renderJson(deps, global, await client.filterColumns(category));
73
85
  }));
74
86
  }
75
- //# sourceMappingURL=units.js.map
@@ -1,3 +1,2 @@
1
1
  #!/usr/bin/env node
2
2
  export {};
3
- //# sourceMappingURL=index.d.ts.map
@@ -8,4 +8,3 @@ run(process.argv.slice(2)).then((code) => {
8
8
  process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}\n`);
9
9
  process.exitCode = 1;
10
10
  });
11
- //# sourceMappingURL=index.js.map
@@ -9,4 +9,3 @@ export interface CliDeps {
9
9
  createClient(options: MastrClientOptions): MastrClient;
10
10
  }
11
11
  export declare const defaultIO: CliIO;
12
- //# sourceMappingURL=io.d.ts.map
@@ -4,4 +4,3 @@ export const defaultIO = {
4
4
  out: (text) => process.stdout.write(text + "\n"),
5
5
  err: (text) => process.stderr.write(text + "\n"),
6
6
  };
7
- //# sourceMappingURL=io.js.map
@@ -4,4 +4,3 @@ export declare const VERSION: string;
4
4
  /** Default dependencies: real client + real stdout/stderr. */
5
5
  export declare const defaultDeps: CliDeps;
6
6
  export declare function buildProgram(deps?: CliDeps): Command;
7
- //# sourceMappingURL=program.d.ts.map
@@ -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>", "retries for transient 429/503 responses (0..10)", parseBoundedInt(0, 10))
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
@@ -1,3 +1,2 @@
1
1
  import type { CliDeps } from "./io.js";
2
2
  export declare function run(argv: string[], deps?: CliDeps): Promise<number>;
3
- //# sourceMappingURL=run.d.ts.map
@@ -82,4 +82,3 @@ export async function run(argv, deps = defaultDeps) {
82
82
  return EXIT.OTHER;
83
83
  }
84
84
  }
85
- //# sourceMappingURL=run.js.map
@@ -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,27 +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
- /** commander value-parser: a non-empty (after trimming) string. */
12
- export declare function parseNonEmpty(value: string): string;
13
12
  /**
14
- * commander value-parser for `--base-url`: non-empty and http/https-only.
15
- *
16
- * The transport already rejects any non-http(s) scheme, so this is defence in
17
- * depth — but doing the check at parse time turns a bad scheme (e.g. `file:` /
18
- * `data:` / `ftp:`) into a friendly usage error (exit 2) with the value the user
19
- * actually typed, rather than a network error (exit 6) echoing the fully built
20
- * request URL. The transport check stays the enforcement point for library callers.
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;
21
+ /**
22
+ * commander value-parser for `--filter`: non-blank, and not a spec the register would
23
+ * misread (see `filterProblem`), so it becomes a usage error before any request.
24
+ */
25
+ export declare function parseFilter(value: string): string;
26
+ /**
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.
21
31
  */
22
- export declare function parseBaseUrl(value: string): string;
32
+ export declare const parseBaseUrl: (value: string) => string;
23
33
  /** Build a commander value-parser for an integer constrained to [min, max]. */
24
34
  export declare function parseBoundedInt(min: number, max: number): (value: string) => number;
25
35
  /**
26
- * commander value-parser for a value that ends up in an HTTP header (User-Agent).
27
- * Rejects control characters — a CR/LF (or other C0/DEL byte) would otherwise reach
28
- * Node's HTTP layer and throw an opaque `ERR_INVALID_CHAR`. Tab (0x09) is allowed;
29
- * checked by char code so the source stays free of control bytes.
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`.
30
40
  */
31
- export declare function parseHeaderValue(value: string): string;
41
+ export declare const parseHeaderValue: (value: string) => string;
32
42
  export interface GlobalOptions {
33
43
  baseUrl?: string;
34
44
  timeout?: number;
@@ -41,9 +51,10 @@ export interface GlobalOptions {
41
51
  /** Translate resolved global CLI options into client options. */
42
52
  export declare function toEngineOptions(global: GlobalOptions): MastrClientOptions;
43
53
  /**
44
- * Escape the control characters JSON.stringify leaves raw. It escapes C0 (including
45
- * ESC) but not DEL or the C1 range U+0080–U+009F, and terminals may act on those —
46
- * U+009B is the 8-bit form of CSI. The output is server data, so escape them; the
54
+ * Escape the characters JSON.stringify leaves raw although a terminal acts on them.
55
+ * It escapes C0 (including ESC) but not DEL, the C1 range U+0080–U+009F (U+009B is
56
+ * the 8-bit form of CSI) or the bidi formatting characters (isBidiControl), which
57
+ * reorder the text that follows. The output is server data, so escape them; the
47
58
  * result is equivalent, valid JSON (these characters only occur inside strings).
48
59
  * Checked by char code so the source stays free of control bytes.
49
60
  */
@@ -68,4 +79,3 @@ export interface ActionContext {
68
79
  * the trailing options object and command instance to recover the positionals.
69
80
  */
70
81
  export declare function action(deps: CliDeps, fn: (ctx: ActionContext, positionals: string[]) => Promise<void>): (...args: unknown[]) => Promise<void>;
71
- //# sourceMappingURL=shared.d.ts.map
@@ -3,6 +3,9 @@
3
3
  import { InvalidArgumentError } from "commander";
4
4
  import { isoifyDates } from "../client/client.js";
5
5
  import { MastrParseError } from "../client/errors.js";
6
+ import { filterProblem } from "../client/filter.js";
7
+ import { isBidiControl } from "../client/engine.js";
8
+ import { baseUrlProblem, headerValueProblem, nonBlankProblem, sortProblem, } from "../client/validate.js";
6
9
  /**
7
10
  * commander value-parser: a plain base-10 non-negative integer.
8
11
  *
@@ -20,39 +23,40 @@ export function parseIntArg(value) {
20
23
  }
21
24
  return n;
22
25
  }
23
- /** commander value-parser: a non-empty (after trimming) string. */
24
- export function parseNonEmpty(value) {
25
- if (value.trim() === "") {
26
- throw new InvalidArgumentError("Expected a non-empty value.");
27
- }
28
- 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
+ };
29
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);
30
42
  /**
31
- * commander value-parser for `--base-url`: non-empty and http/https-only.
32
- *
33
- * The transport already rejects any non-http(s) scheme, so this is defence in
34
- * depth — but doing the check at parse time turns a bad scheme (e.g. `file:` /
35
- * `data:` / `ftp:`) into a friendly usage error (exit 2) with the value the user
36
- * actually typed, rather than a network error (exit 6) echoing the fully built
37
- * request URL. The transport check stays the enforcement point for library callers.
43
+ * commander value-parser for `--filter`: non-blank, and not a spec the register would
44
+ * misread (see `filterProblem`), so it becomes a usage error before any request.
38
45
  */
39
- export function parseBaseUrl(value) {
40
- const trimmed = value.trim();
41
- if (trimmed === "") {
42
- throw new InvalidArgumentError("Expected a non-empty value.");
43
- }
44
- let parsed;
45
- try {
46
- parsed = new URL(trimmed);
47
- }
48
- catch {
49
- throw new InvalidArgumentError("Expected an absolute http(s) URL.");
50
- }
51
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
52
- throw new InvalidArgumentError("Only http and https URLs are supported.");
53
- }
46
+ export function parseFilter(value) {
47
+ parseNonEmpty(value);
48
+ const problem = filterProblem(value);
49
+ if (problem !== undefined)
50
+ throw new InvalidArgumentError(problem);
54
51
  return value;
55
52
  }
53
+ /**
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.
58
+ */
59
+ export const parseBaseUrl = parseWith(baseUrlProblem);
56
60
  /** Build a commander value-parser for an integer constrained to [min, max]. */
57
61
  export function parseBoundedInt(min, max) {
58
62
  return (value) => {
@@ -65,20 +69,12 @@ export function parseBoundedInt(min, max) {
65
69
  };
66
70
  }
67
71
  /**
68
- * commander value-parser for a value that ends up in an HTTP header (User-Agent).
69
- * Rejects control characters — a CR/LF (or other C0/DEL byte) would otherwise reach
70
- * Node's HTTP layer and throw an opaque `ERR_INVALID_CHAR`. Tab (0x09) is allowed;
71
- * checked by char code so the source stays free of control bytes.
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`.
72
76
  */
73
- export function parseHeaderValue(value) {
74
- for (let i = 0; i < value.length; i++) {
75
- const c = value.charCodeAt(i);
76
- if ((c < 0x20 && c !== 0x09) || c === 0x7f) {
77
- throw new InvalidArgumentError("Value contains control characters.");
78
- }
79
- }
80
- return value;
81
- }
77
+ export const parseHeaderValue = parseWith(headerValueProblem);
82
78
  /** Translate resolved global CLI options into client options. */
83
79
  export function toEngineOptions(global) {
84
80
  const options = {};
@@ -95,9 +91,10 @@ export function toEngineOptions(global) {
95
91
  return options;
96
92
  }
97
93
  /**
98
- * Escape the control characters JSON.stringify leaves raw. It escapes C0 (including
99
- * ESC) but not DEL or the C1 range U+0080–U+009F, and terminals may act on those —
100
- * U+009B is the 8-bit form of CSI. The output is server data, so escape them; the
94
+ * Escape the characters JSON.stringify leaves raw although a terminal acts on them.
95
+ * It escapes C0 (including ESC) but not DEL, the C1 range U+0080–U+009F (U+009B is
96
+ * the 8-bit form of CSI) or the bidi formatting characters (isBidiControl), which
97
+ * reorder the text that follows. The output is server data, so escape them; the
101
98
  * result is equivalent, valid JSON (these characters only occur inside strings).
102
99
  * Checked by char code so the source stays free of control bytes.
103
100
  */
@@ -106,7 +103,7 @@ export function escapeControlChars(json) {
106
103
  let from = 0;
107
104
  for (let i = 0; i < json.length; i++) {
108
105
  const c = json.charCodeAt(i);
109
- if (c >= 0x7f && c <= 0x9f) {
106
+ if ((c >= 0x7f && c <= 0x9f) || isBidiControl(c)) {
110
107
  result += json.slice(from, i) + "\\u" + c.toString(16).padStart(4, "0");
111
108
  from = i + 1;
112
109
  }
@@ -152,4 +149,3 @@ export function action(deps, fn) {
152
149
  await fn({ client, global, opts: command.opts() }, positionals);
153
150
  };
154
151
  }
155
- //# sourceMappingURL=shared.js.map
@@ -1,10 +1,16 @@
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
+ /** Largest `page` the client (and the CLI's `--page`) accepts. */
4
+ export declare const MAX_PAGE = 1000000;
5
+ /** Largest `pageSize` the client (and the CLI's `--page-size`) accepts. */
6
+ export declare const MAX_PAGE_SIZE = 5000;
3
7
  /** Options for the MaStR client (engine options only — the API needs no auth). */
4
8
  export type MastrClientOptions = EngineOptions;
5
9
  /**
6
10
  * Parse a MaStR Microsoft-AJAX date string (`"/Date(1548979200000)/"`) into a Date.
7
- * Returns null if the string is not in that format.
11
+ * The offset form `"/Date(1548979200000+0100)/"` is accepted too; its milliseconds
12
+ * are UTC already, the offset only names the sender's zone. Returns null if the
13
+ * string is not in that format or the value is outside the Date range.
8
14
  */
9
15
  export declare function parseMsDate(value: string): Date | null;
10
16
  /**
@@ -19,8 +25,21 @@ export declare class MastrClient {
19
25
  * Fetch one page of units for a category. Sends the full Kendo param set —
20
26
  * `sort`, `page`, `pageSize`, `group`, `filter` — always, because the server
21
27
  * rejects a request with `group`/`filter` missing ("Die Anfrage ist Null.").
28
+ * An unknown category, a `page` outside 1..`MAX_PAGE`, a `pageSize` outside
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.
22
32
  */
23
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>;
24
43
  /** Electricity-generation units (`Stromerzeugung`). */
25
44
  stromerzeugung(query?: UnitQuery): Promise<UnitPage>;
26
45
  /** Electricity-consumption units (`Stromverbrauch`). */
@@ -29,7 +48,10 @@ export declare class MastrClient {
29
48
  gaserzeugung(query?: UnitQuery): Promise<UnitPage>;
30
49
  /** Gas-consumption units (`Gasverbrauch`). */
31
50
  gasverbrauch(query?: UnitQuery): Promise<UnitPage>;
32
- /** The filterable columns (names, types, dropdown codes) for a category. */
51
+ /**
52
+ * The filterable columns (names, types, dropdown codes) for a category. An `Errors`
53
+ * envelope throws `MastrApiError`, any other non-array reply `MastrParseError` —
54
+ * never an empty list, which would read as "this category has no filters".
55
+ */
33
56
  filterColumns(category: UnitCategory): Promise<FilterColumn[]>;
34
57
  }
35
- //# sourceMappingURL=client.d.ts.map