@maschinenlesbar.org/marktstammdatenregister-cli 0.0.7 → 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.
- package/README.md +23 -4
- package/dist/src/cli/commands/units.d.ts.map +1 -1
- package/dist/src/cli/commands/units.js +21 -14
- package/dist/src/cli/commands/units.js.map +1 -1
- package/dist/src/cli/program.d.ts.map +1 -1
- package/dist/src/cli/program.js +1 -1
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/cli/shared.d.ts +9 -3
- package/dist/src/cli/shared.d.ts.map +1 -1
- package/dist/src/cli/shared.js +28 -4
- package/dist/src/cli/shared.js.map +1 -1
- package/dist/src/client/client.d.ts +15 -2
- package/dist/src/client/client.d.ts.map +1 -1
- package/dist/src/client/client.js +84 -18
- package/dist/src/client/client.js.map +1 -1
- package/dist/src/client/engine.d.ts +59 -12
- package/dist/src/client/engine.d.ts.map +1 -1
- package/dist/src/client/engine.js +113 -25
- package/dist/src/client/engine.js.map +1 -1
- package/dist/src/client/errors.d.ts +10 -1
- package/dist/src/client/errors.d.ts.map +1 -1
- package/dist/src/client/errors.js +27 -3
- package/dist/src/client/errors.js.map +1 -1
- package/dist/src/client/filter.d.ts +57 -0
- package/dist/src/client/filter.d.ts.map +1 -0
- package/dist/src/client/filter.js +145 -0
- package/dist/src/client/filter.js.map +1 -0
- package/dist/src/client/http.js +2 -2
- package/dist/src/client/http.js.map +1 -1
- package/dist/src/client/index.d.ts +5 -3
- package/dist/src/client/index.d.ts.map +1 -1
- package/dist/src/client/index.js +4 -3
- package/dist/src/client/index.js.map +1 -1
- package/dist/src/client/types.d.ts +27 -11
- package/dist/src/client/types.d.ts.map +1 -1
- 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
|
-
/**
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* `
|
|
44
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 (
|
|
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":"AAAA,mFAAmF;AACnF,oFAAoF;AACpF,oFAAoF;AACpF,yCAAyC;AAEzC,OAAO,EAAE,iBAAiB,EAAkB,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,gBAAgB,EAAoB,MAAM,YAAY,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;
|
|
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
|
-
/**
|
|
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,
|
|
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} ${
|
|
48
|
+
super(`${head} for ${args.method} ${url}${detailPart}`);
|
|
28
49
|
this.status = args.status;
|
|
29
|
-
this.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
|
-
/**
|
|
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,
|
|
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
|