@gandalan/weblibs 2.0.13 → 2.0.15

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.
@@ -18,20 +18,83 @@
18
18
  * Error thrown for non-ok HTTP responses. Carries the status code so callers
19
19
  * can react to specific statuses (e.g. the automatic 401 retry in fluentApi)
20
20
  * without parsing the message.
21
+ *
22
+ * `body` holds the raw response body (if it could be read) and `detail` the
23
+ * best-effort human-readable reason extracted from it: for a JSON body the
24
+ * first of `ExceptionMessage` / `Message` / `error` / `title` / `detail`
25
+ * (IDAS WebApi and ASP.NET Core ProblemDetails conventions), for a plain-text
26
+ * body the text itself, for HTML or an unreadable body `null`. UI code should
27
+ * show `detail` to the user and log `message` (method, url, status).
28
+ *
29
+ * Use {@link RestError.fromResponse} to build one from a fetch response; the
30
+ * constructor alone cannot read the body (it is asynchronous).
21
31
  */
22
32
  export class RestError extends Error {
23
33
  /**
24
34
  * @param {string} method - HTTP method of the failed request.
25
35
  * @param {string} url - Full URL of the failed request.
26
36
  * @param {Response} res - The non-ok fetch response.
37
+ * @param {string|null} [body=null] - Raw response body, if read.
27
38
  */
28
- constructor(method, url, res) {
39
+ constructor(method, url, res, body = null) {
29
40
  super(`${method} ${url} failed: ${res.status} ${res.statusText}`);
30
41
  this.name = "RestError";
31
42
  this.method = method;
32
43
  this.url = url;
33
44
  this.status = res.status;
34
45
  this.statusText = res.statusText;
46
+ /** @type {string|null} */
47
+ this.body = body;
48
+ /** @type {string|null} */
49
+ this.detail = RestError.extractDetail(body, res.headers.get("content-type"));
50
+ }
51
+
52
+ /**
53
+ * Builds a RestError with the response body read (never throws: an
54
+ * unreadable body just leaves `body`/`detail` at `null`).
55
+ * @param {string} method
56
+ * @param {string} url
57
+ * @param {Response} res
58
+ * @returns {Promise<RestError>}
59
+ */
60
+ static async fromResponse(method, url, res) {
61
+ let body = null;
62
+ try {
63
+ body = await res.text();
64
+ } catch {
65
+ // body already consumed or stream failed – keep null
66
+ }
67
+ return new RestError(method, url, res, body);
68
+ }
69
+
70
+ /**
71
+ * Extracts a human-readable reason from an error body.
72
+ * @param {string|null} body
73
+ * @param {string|null} contentType
74
+ * @returns {string|null}
75
+ */
76
+ static extractDetail(body, contentType) {
77
+ if (!body) return null;
78
+ const text = body.trim();
79
+ if (!text) return null;
80
+ const looksHtml = /^<!doctype html|^<html/i.test(text) || (contentType ?? "").includes("text/html");
81
+ if (looksHtml) return null;
82
+ if (text.startsWith("{") || text.startsWith("[")) {
83
+ try {
84
+ const json = JSON.parse(text);
85
+ if (typeof json === "string") return json;
86
+ if (json && typeof json === "object" && !Array.isArray(json)) {
87
+ for (const key of ["ExceptionMessage", "exceptionMessage", "Message", "message", "error", "Error", "detail", "title"]) {
88
+ const value = json[key];
89
+ if (typeof value === "string" && value.trim()) return value.trim();
90
+ }
91
+ }
92
+ return null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ return text.length <= 500 ? text : null;
35
98
  }
36
99
  }
37
100
 
@@ -92,7 +155,7 @@ export function restClient() {
92
155
  return skipResponseParsing ? res : await this._parseReponse(res);
93
156
  }
94
157
 
95
- throw new RestError("GET", finalUrl, res);
158
+ throw await RestError.fromResponse("GET", finalUrl, res);
96
159
  },
97
160
 
98
161
  /**
@@ -112,7 +175,7 @@ export function restClient() {
112
175
  return skipResponseParsing ? res : await this._parseReponse(res);
113
176
  }
114
177
 
115
- throw new RestError("PUT", finalUrl, res);
178
+ throw await RestError.fromResponse("PUT", finalUrl, res);
116
179
  },
117
180
 
118
181
  /**
@@ -143,7 +206,7 @@ export function restClient() {
143
206
  return skipResponseParsing ? res : await this._parseReponse(res);
144
207
  }
145
208
 
146
- throw new RestError("POST", finalUrl, res);
209
+ throw await RestError.fromResponse("POST", finalUrl, res);
147
210
  },
148
211
 
149
212
  /**
@@ -166,7 +229,7 @@ export function restClient() {
166
229
  return skipResponseParsing ? res : await this._parseReponse(res);
167
230
  }
168
231
 
169
- throw new RestError("DELETE", finalUrl, res);
232
+ throw await RestError.fromResponse("DELETE", finalUrl, res);
170
233
  },
171
234
 
172
235
  _createHeaders(contentType) {
@@ -82,10 +82,10 @@
82
82
  * @typedef {Object} NeherApp3Module
83
83
  * @property {string} moduleName
84
84
  * @property {(context: NeherApp3SetupContext) => void | Promise<void>} [setup]
85
- * @property {(node: HTMLElement, props: NeherApp3SetupContext) => void | function} [mount] - Must return an optional unmount function
85
+ * @property {(node: HTMLElement, props: NeherApp3SetupContext) => void | function} [mount] - Must return an optional unmount function. `node` sits inside the shell's content area, a size container named `content` whose width depends on the navigation layout (sidebars vs. menu flyout), not only on the viewport: lay out with container queries (`@container content (…)`, Tailwind `@3xl/content:`), not viewport breakpoints.
86
86
  * @property {string} [embedUrl]
87
87
  * @property {string[]} [extraCSS]
88
- * @property {boolean} [useShadowDom] - If true, the app will be embedded in a shadow DOM. This is required for CSS isolation. The module then ships all of its styles itself — including **both** themes (`neher`, `neher-dark`), because the shell mirrors `data-theme` onto the module's root inside the shadow root (see `NeherApp3Theme`).
88
+ * @property {boolean} [useShadowDom] - If true, the app will be embedded in a shadow DOM. This is required for CSS isolation. The module then ships all of its styles itself — including **both** themes (`neher`, `neher-dark`), because the shell mirrors `data-theme` onto the module's root inside the shadow root (see `NeherApp3Theme`). Shell-wide base rules do not reach the shadow root either — e.g. the `option` colours that keep native `<select>` popups readable in dark mode.
89
89
  */
90
90
 
91
91
  /**
package/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export function createAuthManager(): FluentAuthManager;
7
7
  export function fluentIdasAuthManager(appToken: string, authBaseUrl: string): FluentAuthManager;
8
8
  export function fetchEnvConfig(envConfig?: string): Promise<EnvironmentConfig>;
9
9
  export function restClient(): FluentRESTClient;
10
- export class RestError extends Error { method: string; url: string; status: number; statusText: string; constructor(method: string, url: string, res: Response); }
10
+ export class RestError extends Error { method: string; url: string; status: number; statusText: string; body: string | null; detail: string | null; constructor(method: string, url: string, res: Response, body?: string | null); static fromResponse(method: string, url: string, res: Response): Promise<RestError>; static extractDetail(body: string | null, contentType: string | null): string | null; }
11
11
 
12
12
  export type AblageApi = {
13
13
  get: (guid: string) => Promise<AblageDTO>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gandalan/weblibs",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "description": "WebLibs for Gandalan JS/TS projects",
5
5
  "keywords": [
6
6
  "gandalan"
@@ -32,7 +32,7 @@ const rootFunctionDeclarationStatements = [
32
32
  "export function fluentIdasAuthManager(appToken: string, authBaseUrl: string): FluentAuthManager;",
33
33
  "export function fetchEnvConfig(envConfig?: string): Promise<EnvironmentConfig>;",
34
34
  "export function restClient(): FluentRESTClient;",
35
- "export class RestError extends Error { method: string; url: string; status: number; statusText: string; constructor(method: string, url: string, res: Response); }"
35
+ "export class RestError extends Error { method: string; url: string; status: number; statusText: string; body: string | null; detail: string | null; constructor(method: string, url: string, res: Response, body?: string | null); static fromResponse(method: string, url: string, res: Response): Promise<RestError>; static extractDetail(body: string | null, contentType: string | null): string | null; }"
36
36
  ];
37
37
 
38
38
  const simpleImportTypePattern = /^import\((?:"|').+(?:"|')\)\.[A-Za-z0-9_$]+$/;