@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
@@ -1,5 +1,24 @@
1
1
  // Error types raised by the client. Kept free of any I/O so they are trivial to
2
2
  // construct in tests and to `instanceof`-check by consumers.
3
+ /**
4
+ * Replace the userinfo of a URL (`https://user:secret@host/...`) with `***`, so a
5
+ * credential in a base URL never reaches an error message, a log or CI output.
6
+ * A URL without userinfo, or one that does not parse, is returned unchanged.
7
+ */
8
+ export function redactUrl(url) {
9
+ let parsed;
10
+ try {
11
+ parsed = new URL(url);
12
+ }
13
+ catch {
14
+ return url;
15
+ }
16
+ if (parsed.username === "" && parsed.password === "")
17
+ return url;
18
+ parsed.username = "***";
19
+ parsed.password = "";
20
+ return parsed.href;
21
+ }
3
22
  /** Base class for every error originating from this client. */
4
23
  export class MastrError extends Error {
5
24
  constructor(message, options) {
@@ -22,11 +41,13 @@ export class MastrApiError extends MastrError {
22
41
  method;
23
42
  body;
24
43
  constructor(args) {
44
+ // The URL is shown without userinfo: a credential in --base-url must not leak.
45
+ const url = redactUrl(args.url);
25
46
  const detailPart = args.detail ? `: ${args.detail}` : "";
26
47
  const head = args.status !== undefined ? `HTTP ${args.status}` : "MaStR error";
27
- super(`${head} for ${args.method} ${args.url}${detailPart}`);
48
+ super(`${head} for ${args.method} ${url}${detailPart}`);
28
49
  this.status = args.status;
29
- this.url = args.url;
50
+ this.url = url;
30
51
  this.method = args.method;
31
52
  this.body = args.body;
32
53
  this.detail = args.detail;
@@ -43,10 +64,14 @@ export class MastrApiError extends MastrError {
43
64
  /** A transport-level failure (DNS, connection reset, timeout, ...). */
44
65
  export class MastrNetworkError extends MastrError {
45
66
  }
46
- /** A client-side validation error (e.g. a bad category) — no request made. */
67
+ /**
68
+ * A client-side validation error — an unknown category, a page or pageSize out of
69
+ * range, a filter the register would misread — thrown before any request, with the
70
+ * message `Invalid <name>: <reason>` (see `assertValid`). The CLI maps it to the
71
+ * usage exit code 2.
72
+ */
47
73
  export class MastrValidationError extends MastrError {
48
74
  }
49
75
  /** The response body could not be parsed as the expected JSON shape. */
50
76
  export class MastrParseError extends MastrError {
51
77
  }
52
- //# sourceMappingURL=errors.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The operators the register's search understands (from its web form, all checked
3
+ * live). `gt`/`lt` are strict and only for `number`/`date` columns; there is no
4
+ * `gte`/`lte`. Any other operator makes the register return 0 rows.
5
+ */
6
+ export declare const FILTER_OPERATORS: readonly ["eq", "neq", "sw", "ct", "nct", "ew", "null", "nn", "gt", "lt"];
7
+ /** A filter operator. */
8
+ export type FilterOperator = (typeof FILTER_OPERATORS)[number];
9
+ /**
10
+ * Describe why the register would misread a filter spec, or return `undefined` when
11
+ * the spec is fine. The spec is read the way the register reads it — split on every
12
+ * `~` into `FilterName`, operator and value, conditions joined by `and` — and must
13
+ * have that shape:
14
+ *
15
+ * - every condition has a non-blank FilterName, a known lower-case operator and a
16
+ * value (`''` for `null`/`nn`: `Ort~null` without a value is ignored upstream and
17
+ * returns the unfiltered register; a blank value for the other operators);
18
+ * - a value that starts with a single quote ends with one;
19
+ * - conditions are joined by `and` only, and nothing dangles at the end.
20
+ *
21
+ * `~or~` is refused: the live search keeps only the part before the first `~or~` and
22
+ * drops every later condition without an error (checked 2026-09-26: wind `2497` alone
23
+ * 43633, `2497~or~…2498` also 43633). An OR between codes of one dropdown column works
24
+ * as a comma list inside one value: `Energieträger~eq~'2497,2498'` (52448 = 43633 + 8815).
25
+ *
26
+ * An unknown FilterName cannot be checked here (the register ignores it and returns
27
+ * the unfiltered set); compare with `filterColumns()`.
28
+ */
29
+ export declare function filterProblem(spec: string): string | undefined;
30
+ /** One condition for {@link buildFilter}. */
31
+ export interface FilterCondition {
32
+ /** The FilterName, e.g. `"Ort"` or `"Energieträger"` (see `filterColumns()`). */
33
+ name: string;
34
+ /** The operator. */
35
+ op: FilterOperator;
36
+ /**
37
+ * The value, quoted by `buildFilter`. For a dropdown column pass its code; an array
38
+ * becomes the comma list the register reads as "any of these codes". Ignored for
39
+ * `null`/`nn` (sent as `''`). Must not contain `~` (nor `,` in an array item).
40
+ */
41
+ value?: string | number | readonly (string | number)[];
42
+ }
43
+ /**
44
+ * Build a filter spec from conditions joined by `~and~`, quoting each value. Unlike
45
+ * string interpolation (`Ort~eq~'${input}'`), a value can't add conditions: one with a
46
+ * `~` (the register's separator, which has no escape) throws `MastrValidationError`, so
47
+ * `Münster'~and~Energieträger~eq~'2497` is refused instead of becoming a second
48
+ * condition.
49
+ *
50
+ * buildFilter([{ name: "Ort", op: "eq", value: "Münster" },
51
+ * { name: "Energieträger", op: "eq", value: ["2497", "2498"] }])
52
+ * // → "Ort~eq~'Münster'~and~Energieträger~eq~'2497,2498'"
53
+ */
54
+ export declare function buildFilter(conditions: readonly FilterCondition[]): string;
55
+ /** Throw a {@link MastrValidationError} if {@link filterProblem} finds a problem. */
56
+ export declare function validateFilter(spec: string): void;
@@ -0,0 +1,144 @@
1
+ // Checks for MaStR filter specs (`FilterName~op~'value'~and~…`), shared by the
2
+ // client (library callers) and the CLI's `--filter` parser. The register never
3
+ // reports a filter it misreads: it answers with a plausible but wrong count (an
4
+ // unfiltered total, a truncated condition list, or 0 rows), so specs it would
5
+ // misread are refused before any request.
6
+ import { MastrValidationError } from "./errors.js";
7
+ /**
8
+ * The operators the register's search understands (from its web form, all checked
9
+ * live). `gt`/`lt` are strict and only for `number`/`date` columns; there is no
10
+ * `gte`/`lte`. Any other operator makes the register return 0 rows.
11
+ */
12
+ export const FILTER_OPERATORS = ["eq", "neq", "sw", "ct", "nct", "ew", "null", "nn", "gt", "lt"];
13
+ /** Operators that test for an empty / non-empty column and ignore their value (send `''`). */
14
+ const UNARY_OPERATORS = new Set(["null", "nn"]);
15
+ const OPERATORS = new Set(FILTER_OPERATORS);
16
+ const SHAPE = "Expected FilterName~op~'value' (e.g. Energieträger~eq~'2495'), several joined by ~and~.";
17
+ /**
18
+ * Describe why the register would misread a filter spec, or return `undefined` when
19
+ * the spec is fine. The spec is read the way the register reads it — split on every
20
+ * `~` into `FilterName`, operator and value, conditions joined by `and` — and must
21
+ * have that shape:
22
+ *
23
+ * - every condition has a non-blank FilterName, a known lower-case operator and a
24
+ * value (`''` for `null`/`nn`: `Ort~null` without a value is ignored upstream and
25
+ * returns the unfiltered register; a blank value for the other operators);
26
+ * - a value that starts with a single quote ends with one;
27
+ * - conditions are joined by `and` only, and nothing dangles at the end.
28
+ *
29
+ * `~or~` is refused: the live search keeps only the part before the first `~or~` and
30
+ * drops every later condition without an error (checked 2026-09-26: wind `2497` alone
31
+ * 43633, `2497~or~…2498` also 43633). An OR between codes of one dropdown column works
32
+ * as a comma list inside one value: `Energieträger~eq~'2497,2498'` (52448 = 43633 + 8815).
33
+ *
34
+ * An unknown FilterName cannot be checked here (the register ignores it and returns
35
+ * the unfiltered set); compare with `filterColumns()`.
36
+ */
37
+ export function filterProblem(spec) {
38
+ const parts = spec.split("~");
39
+ let i = 0;
40
+ for (let n = 1;; n++) {
41
+ const name = parts[i];
42
+ const op = parts[i + 1];
43
+ const value = parts[i + 2];
44
+ if (name === undefined || name.trim() === "") {
45
+ return `Condition ${n} has no FilterName. ${SHAPE}`;
46
+ }
47
+ if (op === undefined || value === undefined) {
48
+ return `Condition ${n} ("${parts.slice(i).join("~")}") is incomplete. ${SHAPE}`;
49
+ }
50
+ if (!OPERATORS.has(op)) {
51
+ const lower = op.trim().toLowerCase();
52
+ const hint = OPERATORS.has(lower) ? ` Operators are lower case: use "${lower}".` : "";
53
+ return (`Unknown operator "${op}" in condition ${n}. The operators are ` +
54
+ `${FILTER_OPERATORS.join(", ")} (gt/lt are strict; there is no gte/lte); the register ` +
55
+ `returns 0 rows for any other.${hint}`);
56
+ }
57
+ if (!UNARY_OPERATORS.has(op) && value.trim() === "") {
58
+ return `Condition ${n} ("${name}~${op}") has no value. ${SHAPE}`;
59
+ }
60
+ if (value.startsWith("'") && (value.length < 2 || !value.endsWith("'"))) {
61
+ // A later part that closes the quote means the value itself held a "~".
62
+ const close = parts.findIndex((part, j) => j > i + 2 && part.endsWith("'"));
63
+ if (close !== -1) {
64
+ const meant = parts.slice(i + 2, close + 1).join("~");
65
+ return (`The value ${meant} in condition ${n} contains "~". A filter value cannot contain "~": ` +
66
+ `the register splits the filter on every "~" and has no escape, so it would read ${value}' ` +
67
+ "and treat the rest as further conditions. Leave the ~ out (e.g. match a part with ct).");
68
+ }
69
+ return `The value of condition ${n} (${value}) has no closing single quote. ${SHAPE}`;
70
+ }
71
+ i += 3;
72
+ if (i >= parts.length)
73
+ return undefined;
74
+ const conjunction = parts[i] ?? "";
75
+ if (conjunction.toLowerCase() === "or") {
76
+ return ('"~or~" is not supported: the register ignores everything after the first ~or~ and ' +
77
+ "returns a wrong count. For several codes of one dropdown column, list them in one " +
78
+ "value: Energieträger~eq~'2497,2498'.");
79
+ }
80
+ if (conjunction !== "and") {
81
+ return `Expected ~and~ after condition ${n}, got "~${conjunction}~". Conditions are joined by ~and~ only.`;
82
+ }
83
+ i += 1;
84
+ if (parts.slice(i).join("~").trim() === "") {
85
+ return 'The filter ends with "~and~": a condition must follow it.';
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Build a filter spec from conditions joined by `~and~`, quoting each value. Unlike
91
+ * string interpolation (`Ort~eq~'${input}'`), a value can't add conditions: one with a
92
+ * `~` (the register's separator, which has no escape) throws `MastrValidationError`, so
93
+ * `Münster'~and~Energieträger~eq~'2497` is refused instead of becoming a second
94
+ * condition.
95
+ *
96
+ * buildFilter([{ name: "Ort", op: "eq", value: "Münster" },
97
+ * { name: "Energieträger", op: "eq", value: ["2497", "2498"] }])
98
+ * // → "Ort~eq~'Münster'~and~Energieträger~eq~'2497,2498'"
99
+ */
100
+ export function buildFilter(conditions) {
101
+ if (!Array.isArray(conditions) || conditions.length === 0) {
102
+ throw new MastrValidationError("Invalid filter: expected at least one condition.");
103
+ }
104
+ const parts = conditions.map((c, index) => {
105
+ const n = index + 1;
106
+ if (typeof c?.name !== "string" || c.name.trim() === "" || c.name.includes("~")) {
107
+ throw new MastrValidationError(`Invalid filter: condition ${n} needs a non-blank FilterName without "~", got ${JSON.stringify(c?.name)}.`);
108
+ }
109
+ if (!OPERATORS.has(c.op)) {
110
+ throw new MastrValidationError(`Invalid filter: unknown operator ${JSON.stringify(c.op)} in condition ${n}; expected one of ${FILTER_OPERATORS.join(", ")}.`);
111
+ }
112
+ if (UNARY_OPERATORS.has(c.op))
113
+ return `${c.name}~${c.op}~''`;
114
+ const items = Array.isArray(c.value) ? c.value : [c.value];
115
+ if (items.length === 0) {
116
+ throw new MastrValidationError(`Invalid filter: condition ${n} ("${c.name}") has an empty value list.`);
117
+ }
118
+ const texts = items.map((item) => {
119
+ if ((typeof item !== "string" && typeof item !== "number") || String(item).trim() === "") {
120
+ throw new MastrValidationError(`Invalid filter: condition ${n} ("${c.name}") needs a non-blank value, got ${JSON.stringify(item)}.`);
121
+ }
122
+ const text = String(item);
123
+ if (text.includes("~")) {
124
+ throw new MastrValidationError(`Invalid filter: the value ${JSON.stringify(text)} in condition ${n} contains "~", which the ` +
125
+ "register reads as a separator (there is no escape).");
126
+ }
127
+ if (items.length > 1 && text.includes(",")) {
128
+ throw new MastrValidationError(`Invalid filter: the list item ${JSON.stringify(text)} in condition ${n} contains ",", which ` +
129
+ "separates the codes of a list.");
130
+ }
131
+ return text;
132
+ });
133
+ return `${c.name}~${c.op}~'${texts.join(",")}'`;
134
+ });
135
+ const spec = parts.join("~and~");
136
+ validateFilter(spec);
137
+ return spec;
138
+ }
139
+ /** Throw a {@link MastrValidationError} if {@link filterProblem} finds a problem. */
140
+ export function validateFilter(spec) {
141
+ const problem = filterProblem(spec);
142
+ if (problem !== undefined)
143
+ throw new MastrValidationError(`Invalid filter: ${problem}`);
144
+ }
@@ -28,4 +28,3 @@ export declare const MAX_TIMEOUT_MS = 2147483647;
28
28
  * (connection errors, timeouts, malformed URLs).
29
29
  */
30
30
  export declare const nodeHttpTransport: Transport;
31
- //# sourceMappingURL=http.d.ts.map
@@ -7,7 +7,7 @@
7
7
  // `http.createServer` in the test-suite.
8
8
  import http from "node:http";
9
9
  import https from "node:https";
10
- import { MastrNetworkError } from "./errors.js";
10
+ import { MastrNetworkError, redactUrl } from "./errors.js";
11
11
  /**
12
12
  * The longest delay Node's timers support (2^31 - 1 ms, about 24.8 days). A longer one
13
13
  * prints a TimeoutOverflowWarning and fires after 1 ms, so timeouts are capped here.
@@ -30,7 +30,7 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
30
30
  // Only http/https are supported. Reject anything else up front with a clear,
31
31
  // typed error instead of letting Node throw an opaque ERR_INVALID_PROTOCOL.
32
32
  if (url.protocol !== "http:" && url.protocol !== "https:") {
33
- reject(new MastrNetworkError(`Unsupported protocol "${url.protocol}" in URL: ${request.url}`));
33
+ reject(new MastrNetworkError(`Unsupported protocol "${url.protocol}" in URL: ${redactUrl(request.url)}`));
34
34
  return;
35
35
  }
36
36
  const isHttps = url.protocol === "https:";
@@ -46,40 +46,51 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
46
46
  };
47
47
  const done = settle(resolve);
48
48
  const fail = settle(reject);
49
- const req = driver.request(url, {
50
- method: request.method,
51
- headers: request.headers,
52
- }, (res) => {
53
- const chunks = [];
54
- let received = 0;
55
- let aborted = false;
56
- res.on("data", (chunk) => {
57
- if (aborted)
58
- return;
59
- received += chunk.length;
60
- if (maxBytes !== undefined && received > maxBytes) {
61
- aborted = true;
62
- res.destroy();
63
- fail(new MastrNetworkError(`Response exceeded maxResponseBytes (${maxBytes})`));
64
- return;
65
- }
66
- chunks.push(chunk);
67
- });
68
- res.on("end", () => {
69
- if (aborted)
70
- return;
71
- done({
72
- status: res.statusCode ?? 0,
73
- headers: res.headers,
74
- body: Buffer.concat(chunks),
49
+ // driver.request() validates the headers synchronously and throws a raw
50
+ // TypeError (ERR_INVALID_CHAR) for a bad one; reject with the typed error instead.
51
+ let req;
52
+ try {
53
+ req = driver.request(url, {
54
+ method: request.method,
55
+ headers: request.headers,
56
+ }, (res) => {
57
+ const chunks = [];
58
+ let received = 0;
59
+ let aborted = false;
60
+ res.on("data", (chunk) => {
61
+ if (aborted)
62
+ return;
63
+ received += chunk.length;
64
+ if (maxBytes !== undefined && received > maxBytes) {
65
+ aborted = true;
66
+ res.destroy();
67
+ fail(new MastrNetworkError(`Response exceeded maxResponseBytes (${maxBytes})`));
68
+ return;
69
+ }
70
+ chunks.push(chunk);
71
+ });
72
+ res.on("end", () => {
73
+ if (aborted)
74
+ return;
75
+ done({
76
+ status: res.statusCode ?? 0,
77
+ headers: res.headers,
78
+ body: Buffer.concat(chunks),
79
+ });
80
+ });
81
+ res.on("error", (err) => {
82
+ if (aborted)
83
+ return; // we already rejected with the size-cap error
84
+ fail(new MastrNetworkError(`Response stream error: ${err.message}`, { cause: err }));
75
85
  });
76
86
  });
77
- res.on("error", (err) => {
78
- if (aborted)
79
- return; // we already rejected with the size-cap error
80
- fail(new MastrNetworkError(`Response stream error: ${err.message}`, { cause: err }));
81
- });
82
- });
87
+ }
88
+ catch (err) {
89
+ reject(new MastrNetworkError(`Invalid request: ${err instanceof Error ? err.message : String(err)}`, {
90
+ cause: err,
91
+ }));
92
+ return;
93
+ }
83
94
  if (request.timeoutMs && request.timeoutMs > 0) {
84
95
  const timeoutMs = request.timeoutMs;
85
96
  timer = setTimeout(() => {
@@ -95,4 +106,3 @@ export const nodeHttpTransport = (request) => new Promise((resolve, reject) => {
95
106
  req.write(request.body);
96
107
  req.end();
97
108
  });
98
- //# sourceMappingURL=http.js.map
@@ -1,11 +1,14 @@
1
- export { MastrClient, parseMsDate, isoifyDates } from "./client.js";
1
+ export { MastrClient, parseMsDate, isoifyDates, MAX_PAGE, MAX_PAGE_SIZE } from "./client.js";
2
2
  export type { MastrClientOptions } from "./client.js";
3
- export { RequestEngine, DEFAULT_BASE_URL } from "./engine.js";
3
+ export { FILTER_OPERATORS, buildFilter, filterProblem, validateFilter } from "./filter.js";
4
+ export type { FilterCondition, FilterOperator } from "./filter.js";
5
+ export { RequestEngine, assertHeaderValue, validateBaseUrl, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, parseRetryAfter, describeMastrErrors, sanitizeServerText, isBidiControl, } from "./engine.js";
4
6
  export type { EngineOptions, RawResponse } from "./engine.js";
5
7
  export { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
6
8
  export type { Transport, HttpRequest, HttpResponse } from "./http.js";
7
9
  export { buildQueryString } from "./query.js";
10
+ export { assertValid, baseUrlProblem, baseUrlWhitespaceProblem, COUNT_IGNORED_KEYS, countQueryProblem, headerNameProblem, headerValueProblem, intRangeProblem, nonBlankProblem, sortProblem, } from "./validate.js";
11
+ export type { Problem } from "./validate.js";
8
12
  export type { QueryParams, QueryValue } from "./query.js";
9
- export { MastrError, MastrApiError, MastrNetworkError, MastrValidationError, MastrParseError, } from "./errors.js";
13
+ export { MastrError, MastrApiError, MastrNetworkError, MastrValidationError, MastrParseError, redactUrl, } from "./errors.js";
10
14
  export * from "./types.js";
11
- //# sourceMappingURL=index.d.ts.map
@@ -1,8 +1,9 @@
1
1
  // Public entry point for the API client library.
2
- export { MastrClient, parseMsDate, isoifyDates } from "./client.js";
3
- export { RequestEngine, DEFAULT_BASE_URL } from "./engine.js";
2
+ export { MastrClient, parseMsDate, isoifyDates, MAX_PAGE, MAX_PAGE_SIZE } from "./client.js";
3
+ export { FILTER_OPERATORS, buildFilter, filterProblem, validateFilter } from "./filter.js";
4
+ export { RequestEngine, assertHeaderValue, validateBaseUrl, DEFAULT_BASE_URL, MAX_RETRIES, MAX_RETRY_AFTER_MS, parseRetryAfter, describeMastrErrors, sanitizeServerText, isBidiControl, } from "./engine.js";
4
5
  export { MAX_TIMEOUT_MS, nodeHttpTransport } from "./http.js";
5
6
  export { buildQueryString } from "./query.js";
6
- export { MastrError, MastrApiError, MastrNetworkError, MastrValidationError, MastrParseError, } from "./errors.js";
7
+ export { assertValid, baseUrlProblem, baseUrlWhitespaceProblem, COUNT_IGNORED_KEYS, countQueryProblem, headerNameProblem, headerValueProblem, intRangeProblem, nonBlankProblem, sortProblem, } from "./validate.js";
8
+ export { MastrError, MastrApiError, MastrNetworkError, MastrValidationError, MastrParseError, redactUrl, } from "./errors.js";
7
9
  export * from "./types.js";
8
- //# sourceMappingURL=index.js.map
@@ -6,4 +6,3 @@ export type QueryParams = Record<string, QueryValue>;
6
6
  * Returns an empty string when no parameters survive filtering.
7
7
  */
8
8
  export declare function buildQueryString(params: QueryParams): string;
9
- //# sourceMappingURL=query.d.ts.map
@@ -30,4 +30,3 @@ export function buildQueryString(params) {
30
30
  // URLSearchParams encodes spaces as "+"; "%20" is more broadly interoperable.
31
31
  return search.toString().replace(/\+/g, "%20");
32
32
  }
33
- //# sourceMappingURL=query.js.map
@@ -8,9 +8,9 @@ export type MastrDate = string;
8
8
  export type UnitCategory = "stromerzeugung" | "stromverbrauch" | "gaserzeugung" | "gasverbrauch";
9
9
  /**
10
10
  * A unit ("Einheit") record. The API returns a very wide row (~90 fields) whose
11
- * populated columns vary by category (a solar unit carries module fields a gas
12
- * consumer does not), so only the broadly-useful, category-independent fields are
13
- * typed; the index signature carries the rest. Natural-person and confidential data
11
+ * columns vary by category (a solar unit carries module fields a gas consumer does
12
+ * not), so only broadly useful fields are typed — each says where it occurs when not
13
+ * in every category; the index signature carries the rest. Natural-person and confidential data
14
14
  * are withheld upstream, so operator names may be anonymised (e.g.
15
15
  * `"natürliche Person (ABR…)"`).
16
16
  */
@@ -23,13 +23,23 @@ export interface MastrUnit {
23
23
  /** Operating status name, e.g. `"In Betrieb"`. */
24
24
  BetriebsStatusName?: string;
25
25
  BetriebsStatusId?: number;
26
- /** Energy carrier name, e.g. `"Solare Strahlungsenergie"`. */
26
+ /** Energy carrier name, e.g. `"Solare Strahlungsenergie"` (`stromerzeugung`). */
27
27
  EnergietraegerName?: string;
28
28
  EnergietraegerId?: number;
29
- /** Gross capacity in kW. */
29
+ /** Gross capacity in kW. Only in `stromerzeugung` rows. */
30
30
  Bruttoleistung?: number;
31
- /** Net rated capacity in kW. */
31
+ /** Net rated capacity in kW. Only in `stromerzeugung` rows. */
32
32
  Nettonennleistung?: number;
33
+ /** Gas generation capacity (`gaserzeugung`; the rows state no unit). */
34
+ Erzeugungsleistung?: number | null;
35
+ /** Gas storage injection capacity in kWh/h (`gaserzeugung`). */
36
+ MaxEinspeicherleistung?: number | null;
37
+ /** Gas storage withdrawal capacity in kWh/h (`gaserzeugung`). */
38
+ MaxAusspeicherleistung?: number | null;
39
+ /** Gas storage working gas volume in kWh (`gaserzeugung`). */
40
+ MaxArbeitsvolumen?: number | null;
41
+ /** Maximum gas intake (`gasverbrauch`; the rows state no unit). */
42
+ MaximaleGasbezugsLeistung?: number | null;
33
43
  Bundesland?: string;
34
44
  Landkreis?: string;
35
45
  Gemeinde?: string;
@@ -59,8 +69,11 @@ export interface UnitResponse {
59
69
  /** Total number of matching units (across all pages). */
60
70
  Total: number;
61
71
  AggregateResults?: unknown;
62
- /** A logical error message (e.g. `"Die Anfrage ist Null."`); null on success. */
63
- Errors?: string | null;
72
+ /**
73
+ * A logical error: a message (e.g. `"Die Anfrage ist Null."`) or a Kendo ModelState
74
+ * object; null on success. The client throws `MastrApiError` for any other value.
75
+ */
76
+ Errors?: string | Record<string, unknown> | null;
64
77
  }
65
78
  /** A page of units plus the total match count — what the client returns. */
66
79
  export interface UnitPage {
@@ -70,6 +83,8 @@ export interface UnitPage {
70
83
  data: MastrUnit[];
71
84
  }
72
85
  /** Query parameters for a unit search. `group` is always sent empty by the client. */
86
+ /** The query `count()` takes: the filter and sort of a `UnitQuery`, no paging. */
87
+ export type CountQuery = Pick<UnitQuery, "filter" | "sort">;
73
88
  export interface UnitQuery {
74
89
  /** 1-based page number (default 1). */
75
90
  page?: number;
@@ -78,9 +93,12 @@ export interface UnitQuery {
78
93
  /** Sort spec: `FieldKey-asc` or `FieldKey-desc`, e.g. `"Bruttoleistung-desc"`. */
79
94
  sort?: string;
80
95
  /**
81
- * Filter spec: `FilterName~op~'value'~[and|or]~…` with ops
82
- * `eq|neq|sw|ct|nct|ew|null|nn`, e.g. `"Energieträger~eq~'2495'"`. Discover the
83
- * `FilterName`s and dropdown codes via {@link MastrClient.filterColumns}.
96
+ * Filter spec: `FilterName~op~'value'~and~…` with ops
97
+ * `eq|neq|sw|ct|nct|ew|null|nn|gt|lt`, e.g. `"Energieträger~eq~'2495'"`. There is
98
+ * no working `~or~` (the register drops everything after it, so the client rejects
99
+ * it); for several codes of one dropdown column list them in one value:
100
+ * `"Energieträger~eq~'2497,2498'"`. Discover the `FilterName`s and dropdown codes
101
+ * via {@link MastrClient.filterColumns}.
84
102
  */
85
103
  filter?: string;
86
104
  }
@@ -100,4 +118,3 @@ export interface FilterColumn {
100
118
  Position?: number | null;
101
119
  OptionalFilterEntity?: unknown;
102
120
  }
103
- //# sourceMappingURL=types.d.ts.map
@@ -1,4 +1,3 @@
1
1
  // Domain types for the Marktstammdatenregister (MaStR) public unit-search API
2
2
  // (www.marktstammdatenregister.de/MaStR).
3
3
  export {};
4
- //# sourceMappingURL=types.js.map
@@ -0,0 +1,63 @@
1
+ import type { UnitQuery } from "./types.js";
2
+ /** A rule: the reason `value` is invalid, or `undefined` when it is valid. */
3
+ export type Problem<T = unknown> = (value: T) => string | undefined;
4
+ /**
5
+ * Throw a {@link MastrValidationError} with the message `Invalid <name>: <reason>`
6
+ * when `problem(value)` finds a reason; otherwise return `value` unchanged. Call it
7
+ * before any request, so a rejected input sends nothing. Async methods call it
8
+ * inside their body, so the rejection arrives as a rejected promise rather than a
9
+ * synchronous throw; constructors throw.
10
+ */
11
+ export declare function assertValid<T>(name: string, value: T, problem: Problem<T>): T;
12
+ /**
13
+ * A rule for an integer option: a safe integer from `min` to `max`. Anything else
14
+ * (a negative, NaN, Infinity, a fraction, a non-number) gets the reason
15
+ * `expected an integer from <min> to <max>, got <value>.`
16
+ */
17
+ export declare function intRangeProblem(min: number, max: number): Problem<number>;
18
+ /**
19
+ * A string that is not blank: `""` or whitespace only is refused, because the
20
+ * register reads an empty parameter as "not set" rather than as an error.
21
+ */
22
+ export declare const nonBlankProblem: Problem<string>;
23
+ /**
24
+ * A rule for a value that goes into an HTTP header (User-Agent, `defaultHeaders`):
25
+ * not blank, no control characters (a CR/LF or other C0 byte, DEL; tab is fine)
26
+ * and no code units above U+00FF. That is what Node's HTTP layer accepts; anything
27
+ * else it refuses with an opaque `ERR_INVALID_CHAR`, and a CR/LF handed to a custom
28
+ * transport could inject a header. Checked by char code so the source stays free
29
+ * of control bytes.
30
+ */
31
+ export declare const headerValueProblem: Problem<string>;
32
+ /** A rule for an HTTP header name: an RFC 9110 token (`X-Trace-Id`). */
33
+ export declare const headerNameProblem: Problem<string>;
34
+ /**
35
+ * A base URL must not carry whitespace or control characters. `new URL()` trims
36
+ * surrounding whitespace and drops tab/CR/LF silently, but the engine joins the raw
37
+ * string to each request path, so "https://h/MaStR " would request `/MaStR%20/...`
38
+ * and a custom transport would see the raw value. Reject rather than guess.
39
+ */
40
+ export declare const baseUrlWhitespaceProblem: Problem<string>;
41
+ /**
42
+ * Every rule for a base URL, in order: a non-blank string, no whitespace or control
43
+ * characters ({@link baseUrlWhitespaceProblem}), an absolute URL, the `http:` or
44
+ * `https:` scheme, and no query or fragment — request paths are appended to the
45
+ * base URL as a string, so a `?` or `#` would swallow every path (`http://h/?x=1`
46
+ * requests `/?x=1/Einheit/...`, `http://h/#f` requests `/`). Userinfo is allowed
47
+ * (error messages redact it). The reasons never echo the URL.
48
+ */
49
+ export declare const baseUrlProblem: Problem<string>;
50
+ /**
51
+ * A `sort` spec (`FieldKey-asc` / `FieldKey-desc`): not blank. A blank one would go
52
+ * out as `sort=` (the same as no sort) or `sort=%20%20`, and an explicitly blank
53
+ * sort is a mistake rather than a request for the default order. The shape itself is
54
+ * left to the register: an unknown sort key there answers 0 rows.
55
+ */
56
+ export declare const sortProblem: Problem<string>;
57
+ /**
58
+ * The `UnitQuery` keys that page the rows. The match count is the same on every
59
+ * page, so `count()` refuses them rather than silently ignore them.
60
+ */
61
+ export declare const COUNT_IGNORED_KEYS: readonly ["page", "pageSize"];
62
+ /** Why `query` cannot be counted: it sets a paging option. */
63
+ export declare const countQueryProblem: Problem<UnitQuery>;