@maschinenlesbar.org/marktstammdatenregister-cli 0.0.6 → 0.0.8

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 (36) hide show
  1. package/README.md +23 -4
  2. package/dist/src/cli/commands/units.d.ts.map +1 -1
  3. package/dist/src/cli/commands/units.js +21 -14
  4. package/dist/src/cli/commands/units.js.map +1 -1
  5. package/dist/src/cli/program.d.ts.map +1 -1
  6. package/dist/src/cli/program.js +1 -1
  7. package/dist/src/cli/program.js.map +1 -1
  8. package/dist/src/cli/shared.d.ts +9 -3
  9. package/dist/src/cli/shared.d.ts.map +1 -1
  10. package/dist/src/cli/shared.js +28 -4
  11. package/dist/src/cli/shared.js.map +1 -1
  12. package/dist/src/client/client.d.ts +15 -2
  13. package/dist/src/client/client.d.ts.map +1 -1
  14. package/dist/src/client/client.js +84 -18
  15. package/dist/src/client/client.js.map +1 -1
  16. package/dist/src/client/engine.d.ts +59 -12
  17. package/dist/src/client/engine.d.ts.map +1 -1
  18. package/dist/src/client/engine.js +113 -25
  19. package/dist/src/client/engine.js.map +1 -1
  20. package/dist/src/client/errors.d.ts +10 -1
  21. package/dist/src/client/errors.d.ts.map +1 -1
  22. package/dist/src/client/errors.js +27 -3
  23. package/dist/src/client/errors.js.map +1 -1
  24. package/dist/src/client/filter.d.ts +57 -0
  25. package/dist/src/client/filter.d.ts.map +1 -0
  26. package/dist/src/client/filter.js +145 -0
  27. package/dist/src/client/filter.js.map +1 -0
  28. package/dist/src/client/http.js +2 -2
  29. package/dist/src/client/http.js.map +1 -1
  30. package/dist/src/client/index.d.ts +5 -3
  31. package/dist/src/client/index.d.ts.map +1 -1
  32. package/dist/src/client/index.js +4 -3
  33. package/dist/src/client/index.js.map +1 -1
  34. package/dist/src/client/types.d.ts +27 -11
  35. package/dist/src/client/types.d.ts.map +1 -1
  36. package/package.json +1 -1
@@ -20,9 +20,13 @@ export interface EngineOptions {
20
20
  * only idle gaps (0 disables; capped at MAX_TIMEOUT_MS, 2^31 - 1 ms).
21
21
  */
22
22
  timeoutMs?: number;
23
- /** Number of automatic retries for transient (429/503) responses. */
23
+ /**
24
+ * Number of automatic retries for transient (429/503) responses. Each waits the
25
+ * response's `Retry-After` (up to `MAX_RETRY_AFTER_MS`; a longer one is not
26
+ * retried), or else `retryDelayMs * attempt`.
27
+ */
24
28
  maxRetries?: number;
25
- /** Base backoff between retries in milliseconds (grows linearly). */
29
+ /** Base backoff between retries in milliseconds (grows linearly); used without a Retry-After. */
26
30
  retryDelayMs?: number;
27
31
  /**
28
32
  * Hard cap on response body size in bytes (defends against memory exhaustion
@@ -33,17 +37,56 @@ export interface EngineOptions {
33
37
  sleep?: (ms: number) => Promise<void>;
34
38
  }
35
39
  /**
36
- * Strip control characters (C0 except tab/newline, DEL, and C1) out of a string
37
- * that originates in an attacker-controlled response — the error `detail` and the
38
- * echoed Content-Type. `JSON.parse` decodes an escaped ESC in an error body into a
39
- * real ESC byte, so without this a hostile/MITM'd endpoint could drive ANSI/OSC
40
- * escape sequences into the user's terminal when the message is printed to stderr.
41
- * This only needs to cover text that flows into an error message: the CLI's JSON
42
- * output is escaped separately (`escapeControlChars` in cli/shared.ts), as
43
- * `JSON.stringify` alone leaves DEL and the C1 range raw. Implemented as a code-point
44
- * filter so no raw control byte ever appears in this source file.
40
+ * Longest `Retry-After` the engine waits out before retrying a 429/503. When the
41
+ * server asks for longer, the engine does not retry at all and surfaces the error at
42
+ * once: retrying early would only land inside the window the server asked us to wait
43
+ * out, and a hostile value must not stall the CLI.
44
+ */
45
+ export declare const MAX_RETRY_AFTER_MS = 30000;
46
+ /**
47
+ * Parse a `Retry-After` header into a delay in milliseconds (RFC 9110 §10.2.3):
48
+ * either delay-seconds (`"120"`) or an HTTP-date (`"Wed, 21 Oct 2026 07:28:00 GMT"`,
49
+ * turned into the time left from `now`; a date in the past gives 0).
50
+ *
51
+ * Returns `undefined` when the header is absent or malformed — negative (`"-1"`),
52
+ * fractional (`"1.5"`), padded inside, any other date format — so the caller falls
53
+ * back to its own backoff. The strict patterns matter: `Date.parse` alone would
54
+ * read `"1.5"` as a date in 2001 and retry at once.
55
+ */
56
+ export declare function parseRetryAfter(header: string | string[] | undefined, now?: number): number | undefined;
57
+ /**
58
+ * True for the Unicode bidirectional formatting characters: ALM (U+061C), LRM/RLM
59
+ * (U+200E/U+200F), the embeddings and overrides U+202A–U+202E and the isolates
60
+ * U+2066–U+2069. A terminal applies them to the text that follows, so an override
61
+ * in server text can reorder what the user sees ("Trojan Source" spoofing).
62
+ */
63
+ export declare function isBidiControl(code: number): boolean;
64
+ /**
65
+ * Make a string that originates in an attacker-controlled response — the error
66
+ * `detail` (a JSON field, an `Errors` value or a text snippet) and the echoed
67
+ * Content-Type — safe to print into an error message on stderr:
68
+ *
69
+ * - C0 and C1 controls and DEL are dropped. `JSON.parse` decodes an escaped ESC into
70
+ * a real ESC byte; printed raw, a hostile or MITM'd endpoint could drive ANSI/OSC
71
+ * sequences into the terminal.
72
+ * - Bidi formatting characters (isBidiControl) are dropped, so server text cannot
73
+ * reorder the visible message.
74
+ * - Every run of whitespace — newlines, tabs, U+2028/U+2029 included — becomes one
75
+ * space and the ends are trimmed, so the text stays on one line and a server
76
+ * cannot forge an `Error:` line of its own.
77
+ *
78
+ * The CLI's JSON output is escaped separately (`escapeControlChars` in
79
+ * cli/shared.ts): `JSON.stringify` alone leaves DEL, C1 and bidi characters raw.
80
+ * Written as a code-point filter so no raw control byte appears in this source.
45
81
  */
46
82
  export declare function sanitizeServerText(text: string): string;
83
+ /**
84
+ * Describe a Kendo `Errors` value for an error message: a string as is; otherwise
85
+ * (a ModelState object such as `{"": {"errors": ["Invalid filter"]}}`, or an array)
86
+ * every string found in it, sanitised, blanks and repeats dropped, joined "; ".
87
+ * Returns `undefined` when nothing readable is left.
88
+ */
89
+ export declare function describeMastrErrors(errors: unknown): string | undefined;
47
90
  export declare class RequestEngine {
48
91
  private readonly baseUrl;
49
92
  private readonly transport;
@@ -63,7 +106,11 @@ export declare class RequestEngine {
63
106
  * base URL bouncing to a portal page) surfaces as an error.
64
107
  */
65
108
  request(path: string, query?: QueryParams, accept?: string): Promise<RawResponse>;
66
- /** GET a path with query params and parse the JSON reply into `T`. */
109
+ /**
110
+ * GET a path with query params and parse the JSON reply into `T`. Every MaStR
111
+ * endpoint answers with a JSON document, so an empty body or a 204 is a
112
+ * `MastrParseError`, never a silent `null`.
113
+ */
67
114
  getJson<T>(path: string, query?: QueryParams): Promise<T>;
68
115
  private toApiError;
69
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/client/engine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAGhE,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAG/E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQvD;AAyBD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEtD,YAAY,OAAO,GAAE,aAAkB,EAWtC;IAED,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CAIlD;IAED;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,EAAE,MAAM,SAAqB,GAAG,OAAO,CAAC,WAAW,CAAC,CAoClG;IAED,sEAAsE;IAChE,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAW9D;IAED,OAAO,CAAC,UAAU;CAsBnB"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/client/engine.ts"],"names":[],"mappings":"AAKA,OAAO,EAAqB,KAAK,SAAS,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAGhE,eAAO,MAAM,gBAAgB,iDAAiD,CAAC;AAG/E,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iGAAiG;IACjG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAID;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAAS,CAAC;AAMzC;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EACrC,GAAG,GAAE,MAAmB,GACvB,MAAM,GAAG,SAAS,CAOpB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQnD;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASvD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAYvE;AA+BD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAyB;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAS;IAC1C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IAEtD,YAAY,OAAO,GAAE,aAAkB,EAWtC;IAED,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,MAAM,CAIlD;IAED;;;;OAIG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,EAAE,MAAM,SAAqB,GAAG,OAAO,CAAC,WAAW,CAAC,CAyClG;IAED;;;;OAIG;IACG,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,CAW9D;IAED,OAAO,CAAC,UAAU;CAsBnB"}
@@ -4,36 +4,112 @@
4
4
  // parameters travel in the query string.
5
5
  import { nodeHttpTransport } from "./http.js";
6
6
  import { buildQueryString } from "./query.js";
7
- import { MastrApiError, MastrNetworkError, MastrParseError } from "./errors.js";
7
+ import { MastrApiError, MastrNetworkError, MastrParseError, redactUrl } from "./errors.js";
8
8
  export const DEFAULT_BASE_URL = "https://www.marktstammdatenregister.de/MaStR";
9
9
  const DEFAULT_USER_AGENT = "marktstammdatenregister-cli";
10
10
  const DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
11
11
  /**
12
- * Strip control characters (C0 except tab/newline, DEL, and C1) out of a string
13
- * that originates in an attacker-controlled response — the error `detail` and the
14
- * echoed Content-Type. `JSON.parse` decodes an escaped ESC in an error body into a
15
- * real ESC byte, so without this a hostile/MITM'd endpoint could drive ANSI/OSC
16
- * escape sequences into the user's terminal when the message is printed to stderr.
17
- * This only needs to cover text that flows into an error message: the CLI's JSON
18
- * output is escaped separately (`escapeControlChars` in cli/shared.ts), as
19
- * `JSON.stringify` alone leaves DEL and the C1 range raw. Implemented as a code-point
20
- * filter so no raw control byte ever appears in this source file.
12
+ * Longest `Retry-After` the engine waits out before retrying a 429/503. When the
13
+ * server asks for longer, the engine does not retry at all and surfaces the error at
14
+ * once: retrying early would only land inside the window the server asked us to wait
15
+ * out, and a hostile value must not stall the CLI.
16
+ */
17
+ export const MAX_RETRY_AFTER_MS = 30_000;
18
+ /** An IMF-fixdate (RFC 9110 §5.6.7), the one HTTP-date form senders must generate. */
19
+ const IMF_FIXDATE = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
20
+ /**
21
+ * Parse a `Retry-After` header into a delay in milliseconds (RFC 9110 §10.2.3):
22
+ * either delay-seconds (`"120"`) or an HTTP-date (`"Wed, 21 Oct 2026 07:28:00 GMT"`,
23
+ * turned into the time left from `now`; a date in the past gives 0).
24
+ *
25
+ * Returns `undefined` when the header is absent or malformed — negative (`"-1"`),
26
+ * fractional (`"1.5"`), padded inside, any other date format — so the caller falls
27
+ * back to its own backoff. The strict patterns matter: `Date.parse` alone would
28
+ * read `"1.5"` as a date in 2001 and retry at once.
29
+ */
30
+ export function parseRetryAfter(header, now = Date.now()) {
31
+ const value = (Array.isArray(header) ? header[0] : header)?.trim();
32
+ if (value === undefined || value === "")
33
+ return undefined;
34
+ if (/^\d+$/.test(value))
35
+ return Number(value) * 1000;
36
+ if (!IMF_FIXDATE.test(value))
37
+ return undefined;
38
+ const when = Date.parse(value);
39
+ return Number.isNaN(when) ? undefined : Math.max(0, when - now);
40
+ }
41
+ /**
42
+ * True for the Unicode bidirectional formatting characters: ALM (U+061C), LRM/RLM
43
+ * (U+200E/U+200F), the embeddings and overrides U+202A–U+202E and the isolates
44
+ * U+2066–U+2069. A terminal applies them to the text that follows, so an override
45
+ * in server text can reorder what the user sees ("Trojan Source" spoofing).
46
+ */
47
+ export function isBidiControl(code) {
48
+ return (code === 0x061c ||
49
+ code === 0x200e ||
50
+ code === 0x200f ||
51
+ (code >= 0x202a && code <= 0x202e) ||
52
+ (code >= 0x2066 && code <= 0x2069));
53
+ }
54
+ /**
55
+ * Make a string that originates in an attacker-controlled response — the error
56
+ * `detail` (a JSON field, an `Errors` value or a text snippet) and the echoed
57
+ * Content-Type — safe to print into an error message on stderr:
58
+ *
59
+ * - C0 and C1 controls and DEL are dropped. `JSON.parse` decodes an escaped ESC into
60
+ * a real ESC byte; printed raw, a hostile or MITM'd endpoint could drive ANSI/OSC
61
+ * sequences into the terminal.
62
+ * - Bidi formatting characters (isBidiControl) are dropped, so server text cannot
63
+ * reorder the visible message.
64
+ * - Every run of whitespace — newlines, tabs, U+2028/U+2029 included — becomes one
65
+ * space and the ends are trimmed, so the text stays on one line and a server
66
+ * cannot forge an `Error:` line of its own.
67
+ *
68
+ * The CLI's JSON output is escaped separately (`escapeControlChars` in
69
+ * cli/shared.ts): `JSON.stringify` alone leaves DEL, C1 and bidi characters raw.
70
+ * Written as a code-point filter so no raw control byte appears in this source.
21
71
  */
22
72
  export function sanitizeServerText(text) {
23
73
  let out = "";
24
74
  for (const ch of text) {
25
75
  const n = ch.codePointAt(0) ?? 0;
26
- if (n <= 8 || (n >= 0x0b && n <= 0x1f) || (n >= 0x7f && n <= 0x9f))
76
+ const whitespaceControl = n >= 0x09 && n <= 0x0d;
77
+ if (!whitespaceControl && (n <= 0x1f || (n >= 0x7f && n <= 0x9f) || isBidiControl(n)))
27
78
  continue;
28
79
  out += ch;
29
80
  }
30
- return out;
81
+ return out.replace(/\s+/g, " ").trim();
31
82
  }
32
83
  /**
33
- * Reject a base URL whose scheme is not http(s). The default transport already
34
- * gates this per hop, but the engine is exported as a library and may be handed a
35
- * custom transport that does no such check, so gate the configured base URL here
36
- * too (a `file:`/`ftp:` base URL fails fast with a typed error).
84
+ * Describe a Kendo `Errors` value for an error message: a string as is; otherwise
85
+ * (a ModelState object such as `{"": {"errors": ["Invalid filter"]}}`, or an array)
86
+ * every string found in it, sanitised, blanks and repeats dropped, joined "; ".
87
+ * Returns `undefined` when nothing readable is left.
88
+ */
89
+ export function describeMastrErrors(errors) {
90
+ const found = [];
91
+ const walk = (value, depth) => {
92
+ if (typeof value === "string") {
93
+ const text = sanitizeServerText(value);
94
+ if (text !== "" && !found.includes(text))
95
+ found.push(text);
96
+ }
97
+ else if (value !== null && typeof value === "object" && depth < 5) {
98
+ for (const v of Object.values(value))
99
+ walk(v, depth + 1);
100
+ }
101
+ };
102
+ walk(errors, 0);
103
+ return found.length > 0 ? found.join("; ") : undefined;
104
+ }
105
+ /**
106
+ * Reject a base URL whose scheme is not http(s), or that has a query or fragment.
107
+ * The default transport already gates the scheme per hop, but the engine is
108
+ * exported as a library and may be handed a custom transport that does no such
109
+ * check, so gate the configured base URL here too (a `file:`/`ftp:` base URL fails
110
+ * fast with a typed error). Request paths are appended to the base URL as a string,
111
+ * so a `?` or `#` in it would swallow every path: `http://h/?x=1` requests
112
+ * `/?x=1/Einheit/...` and `http://h/#f` requests `/`.
37
113
  */
38
114
  function assertHttpScheme(baseUrl) {
39
115
  let url;
@@ -41,10 +117,13 @@ function assertHttpScheme(baseUrl) {
41
117
  url = new URL(baseUrl);
42
118
  }
43
119
  catch {
44
- throw new MastrNetworkError(`Invalid base URL: ${baseUrl}`);
120
+ throw new MastrNetworkError(`Invalid base URL: ${redactUrl(baseUrl)}`);
45
121
  }
46
122
  if (url.protocol !== "http:" && url.protocol !== "https:") {
47
- throw new MastrNetworkError(`Unsupported protocol "${url.protocol}" in base URL: ${baseUrl}`);
123
+ throw new MastrNetworkError(`Unsupported protocol "${url.protocol}" in base URL: ${redactUrl(baseUrl)}`);
124
+ }
125
+ if (/[?#]/.test(baseUrl)) {
126
+ throw new MastrNetworkError(`Base URL must not contain a query or fragment: ${redactUrl(baseUrl)}`);
48
127
  }
49
128
  }
50
129
  const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -103,9 +182,14 @@ export class RequestEngine {
103
182
  const status = response.status;
104
183
  const retryable = status === 429 || status === 503;
105
184
  if (retryable && attempt < this.maxRetries) {
106
- attempt += 1;
107
- await this.sleep(this.retryDelayMs * attempt);
108
- continue;
185
+ // Honour Retry-After; without a usable one, back off linearly. A Retry-After
186
+ // beyond MAX_RETRY_AFTER_MS is not retried: the error below surfaces at once.
187
+ const retryAfter = parseRetryAfter(response.headers["retry-after"]);
188
+ if (retryAfter === undefined || retryAfter <= MAX_RETRY_AFTER_MS) {
189
+ attempt += 1;
190
+ await this.sleep(retryAfter ?? this.retryDelayMs * attempt);
191
+ continue;
192
+ }
109
193
  }
110
194
  const contentType = String(response.headers["content-type"] ?? "");
111
195
  if (status < 200 || status >= 300) {
@@ -114,12 +198,16 @@ export class RequestEngine {
114
198
  return { data: response.body, contentType, status };
115
199
  }
116
200
  }
117
- /** GET a path with query params and parse the JSON reply into `T`. */
201
+ /**
202
+ * GET a path with query params and parse the JSON reply into `T`. Every MaStR
203
+ * endpoint answers with a JSON document, so an empty body or a 204 is a
204
+ * `MastrParseError`, never a silent `null`.
205
+ */
118
206
  async getJson(path, query) {
119
207
  const res = await this.request(path, query);
120
208
  const text = res.data.toString("utf8");
121
209
  if (res.status === 204 || text.trim().length === 0) {
122
- return null;
210
+ throw new MastrParseError(`Empty response body from ${path}`);
123
211
  }
124
212
  try {
125
213
  return JSON.parse(text);
@@ -133,8 +221,8 @@ export class RequestEngine {
133
221
  let detail;
134
222
  try {
135
223
  const parsed = JSON.parse(text);
136
- if (typeof parsed?.Errors === "string")
137
- detail = parsed.Errors;
224
+ if (parsed?.Errors !== undefined && parsed.Errors !== null)
225
+ detail = describeMastrErrors(parsed.Errors);
138
226
  else if (typeof parsed?.message === "string")
139
227
  detail = parsed.message;
140
228
  else if (typeof parsed?.detail === "string")
@@ -1 +1 @@
1
- {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/client/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../../src/client/engine.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Replace the userinfo of a URL (`https://user:secret@host/...`) with `***`, so a
3
+ * credential in a base URL never reaches an error message, a log or CI output.
4
+ * A URL without userinfo, or one that does not parse, is returned unchanged.
5
+ */
6
+ export declare function redactUrl(url: string): string;
1
7
  /** Base class for every error originating from this client. */
2
8
  export declare class MastrError extends Error {
3
9
  constructor(message: string, options?: {
@@ -33,7 +39,10 @@ export declare class MastrApiError extends MastrError {
33
39
  /** A transport-level failure (DNS, connection reset, timeout, ...). */
34
40
  export declare class MastrNetworkError extends MastrError {
35
41
  }
36
- /** A client-side validation error (e.g. a bad category) — no request made. */
42
+ /**
43
+ * A client-side validation error — an unknown category, a page or pageSize out of
44
+ * range, a filter the register would misread — thrown before any request.
45
+ */
37
46
  export declare class MastrValidationError extends MastrError {
38
47
  }
39
48
  /** The response body could not be parsed as the expected JSON shape. */
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/client/errors.ts"],"names":[],"mappings":"AAGA,+DAA+D;AAC/D,qBAAa,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAGzD;CACF;AAED;;;;;;;GAOG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE;QAChB,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,EASA;IAED,yEAAyE;IACzE,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,2CAA2C;IAC3C,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED,uEAAuE;AACvE,qBAAa,iBAAkB,SAAQ,UAAU;CAAG;AAEpD,8EAA8E;AAC9E,qBAAa,oBAAqB,SAAQ,UAAU;CAAG;AAEvD,wEAAwE;AACxE,qBAAa,eAAgB,SAAQ,UAAU;CAAG"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/client/errors.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAW7C;AAED,+DAA+D;AAC/D,qBAAa,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAGzD;CACF;AAED;;;;;;;GAOG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE;QAChB,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,EAWA;IAED,yEAAyE;IACzE,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED,2CAA2C;IAC3C,IAAI,UAAU,IAAI,OAAO,CAExB;CACF;AAED,uEAAuE;AACvE,qBAAa,iBAAkB,SAAQ,UAAU;CAAG;AAEpD;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,UAAU;CAAG;AAEvD,wEAAwE;AACxE,qBAAa,eAAgB,SAAQ,UAAU;CAAG"}
@@ -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,7 +64,10 @@ 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.
70
+ */
47
71
  export class MastrValidationError extends MastrError {
48
72
  }
49
73
  /** The response body could not be parsed as the expected JSON shape. */
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/client/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAE7D,+DAA+D;AAC/D,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,aAAc,SAAQ,UAAU;IAClC,MAAM,CAAqB;IAC3B,MAAM,CAAqB;IAC3B,GAAG,CAAS;IACZ,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,IAMX;QACC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;QAC/E,KAAK,CAAC,GAAG,IAAI,QAAQ,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,GAAG,UAAU,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,yEAAyE;IACzE,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;IACpD,CAAC;IAED,2CAA2C;IAC3C,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;IAC7B,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,OAAO,iBAAkB,SAAQ,UAAU;CAAG;AAEpD,8EAA8E;AAC9E,MAAM,OAAO,oBAAqB,SAAQ,UAAU;CAAG;AAEvD,wEAAwE;AACxE,MAAM,OAAO,eAAgB,SAAQ,UAAU;CAAG"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../../src/client/errors.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,6DAA6D;AAE7D;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE;QAAE,OAAO,GAAG,CAAC;IACjE,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC;IACxB,MAAM,CAAC,QAAQ,GAAG,EAAE,CAAC;IACrB,OAAO,MAAM,CAAC,IAAI,CAAC;AACrB,CAAC;AAED,+DAA+D;AAC/D,MAAM,OAAO,UAAW,SAAQ,KAAK;IACnC,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED;;;;;;;GAOG;AACH,MAAM,OAAO,aAAc,SAAQ,UAAU;IAClC,MAAM,CAAqB;IAC3B,MAAM,CAAqB;IAC3B,GAAG,CAAS;IACZ,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,IAMX;QACC,+EAA+E;QAC/E,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzD,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;QAC/E,KAAK,CAAC,GAAG,IAAI,QAAQ,IAAI,CAAC,MAAM,IAAI,GAAG,GAAG,UAAU,EAAE,CAAC,CAAC;QACxD,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,CAAC;IAED,yEAAyE;IACzE,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;IACpD,CAAC;IAED,2CAA2C;IAC3C,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;IAC7B,CAAC;CACF;AAED,uEAAuE;AACvE,MAAM,OAAO,iBAAkB,SAAQ,UAAU;CAAG;AAEpD;;;GAGG;AACH,MAAM,OAAO,oBAAqB,SAAQ,UAAU;CAAG;AAEvD,wEAAwE;AACxE,MAAM,OAAO,eAAgB,SAAQ,UAAU;CAAG"}
@@ -0,0 +1,57 @@
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;
57
+ //# sourceMappingURL=filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../../../src/client/filter.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,YAAI,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAC;AAE1G,yBAAyB;AACzB,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;AAU/D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAwD9D;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB;IACpB,EAAE,EAAE,cAAc,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,SAAS,eAAe,EAAE,GAAG,MAAM,CA+C1E;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGjD"}
@@ -0,0 +1,145 @@
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
+ }
145
+ //# sourceMappingURL=filter.js.map