@ignfab/geocontext 0.9.6 → 0.9.7

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 (126) hide show
  1. package/README.md +334 -257
  2. package/dist/gpf/adminexpress.d.ts +17 -8
  3. package/dist/gpf/adminexpress.js +40 -17
  4. package/dist/gpf/adminexpress.js.map +1 -1
  5. package/dist/gpf/altitude.d.ts +21 -9
  6. package/dist/gpf/altitude.js +5 -5
  7. package/dist/gpf/altitude.js.map +1 -1
  8. package/dist/gpf/geocode.d.ts +25 -4
  9. package/dist/gpf/geocode.js +5 -5
  10. package/dist/gpf/geocode.js.map +1 -1
  11. package/dist/gpf/parcellaire-express.d.ts +19 -9
  12. package/dist/gpf/parcellaire-express.js +59 -26
  13. package/dist/gpf/parcellaire-express.js.map +1 -1
  14. package/dist/gpf/urbanisme.d.ts +24 -16
  15. package/dist/gpf/urbanisme.js +81 -33
  16. package/dist/gpf/urbanisme.js.map +1 -1
  17. package/dist/gpf/{wfs.js → wfs-schema-catalog.js} +1 -1
  18. package/dist/gpf/wfs-schema-catalog.js.map +1 -0
  19. package/dist/helpers/RateLimiter.d.ts +44 -0
  20. package/dist/helpers/RateLimiter.js +52 -0
  21. package/dist/helpers/RateLimiter.js.map +1 -0
  22. package/dist/helpers/distance.d.ts +2 -1
  23. package/dist/helpers/distance.js +2 -1
  24. package/dist/helpers/distance.js.map +1 -1
  25. package/dist/helpers/errors/toolError.d.ts +30 -0
  26. package/dist/helpers/errors/toolError.js +193 -0
  27. package/dist/helpers/errors/toolError.js.map +1 -0
  28. package/dist/helpers/errors/zodErrorMapFr.d.ts +20 -0
  29. package/dist/helpers/errors/zodErrorMapFr.js +191 -0
  30. package/dist/helpers/errors/zodErrorMapFr.js.map +1 -0
  31. package/dist/helpers/http.d.ts +67 -7
  32. package/dist/helpers/http.js +458 -84
  33. package/dist/helpers/http.js.map +1 -1
  34. package/dist/helpers/jsonSchema.d.ts +16 -4
  35. package/dist/helpers/jsonSchema.js +7 -1
  36. package/dist/helpers/jsonSchema.js.map +1 -1
  37. package/dist/helpers/schemas.d.ts +4 -4
  38. package/dist/helpers/wfs_engine/attributeFilter.d.ts +51 -0
  39. package/dist/helpers/wfs_engine/attributeFilter.js +258 -0
  40. package/dist/helpers/wfs_engine/attributeFilter.js.map +1 -0
  41. package/dist/helpers/wfs_engine/byId.d.ts +76 -0
  42. package/dist/helpers/wfs_engine/byId.js +106 -0
  43. package/dist/helpers/wfs_engine/byId.js.map +1 -0
  44. package/dist/helpers/wfs_engine/execution.d.ts +72 -0
  45. package/dist/helpers/wfs_engine/execution.js +95 -0
  46. package/dist/helpers/wfs_engine/execution.js.map +1 -0
  47. package/dist/helpers/wfs_engine/features.d.ts +64 -0
  48. package/dist/helpers/wfs_engine/features.js +138 -0
  49. package/dist/helpers/wfs_engine/features.js.map +1 -0
  50. package/dist/helpers/wfs_engine/geometry.d.ts +16 -0
  51. package/dist/helpers/wfs_engine/geometry.js +44 -0
  52. package/dist/helpers/wfs_engine/geometry.js.map +1 -0
  53. package/dist/helpers/wfs_engine/properties.d.ts +51 -0
  54. package/dist/helpers/wfs_engine/properties.js +128 -0
  55. package/dist/helpers/wfs_engine/properties.js.map +1 -0
  56. package/dist/helpers/wfs_engine/queryPreparation.d.ts +32 -0
  57. package/dist/helpers/wfs_engine/queryPreparation.js +149 -0
  58. package/dist/helpers/wfs_engine/queryPreparation.js.map +1 -0
  59. package/dist/helpers/{wfs_internal → wfs_engine}/request.d.ts +49 -2
  60. package/dist/helpers/{wfs_internal → wfs_engine}/request.js +77 -1
  61. package/dist/helpers/wfs_engine/request.js.map +1 -0
  62. package/dist/helpers/wfs_engine/response.d.ts +80 -0
  63. package/dist/helpers/wfs_engine/response.js +135 -0
  64. package/dist/helpers/wfs_engine/response.js.map +1 -0
  65. package/dist/helpers/wfs_engine/schema.d.ts +209 -0
  66. package/dist/helpers/{wfs_internal → wfs_engine}/schema.js +50 -10
  67. package/dist/helpers/wfs_engine/schema.js.map +1 -0
  68. package/dist/helpers/wfs_engine/spatialCql.d.ts +46 -0
  69. package/dist/helpers/wfs_engine/spatialCql.js +54 -0
  70. package/dist/helpers/wfs_engine/spatialCql.js.map +1 -0
  71. package/dist/helpers/wfs_engine/spatialFilter.d.ts +14 -0
  72. package/dist/helpers/wfs_engine/spatialFilter.js +131 -0
  73. package/dist/helpers/wfs_engine/spatialFilter.js.map +1 -0
  74. package/dist/index.js +65 -23
  75. package/dist/index.js.map +1 -1
  76. package/dist/logger.d.ts +1 -1
  77. package/dist/logger.js +4 -1
  78. package/dist/logger.js.map +1 -1
  79. package/dist/tools/AdminexpressTool.d.ts +42 -33
  80. package/dist/tools/AdminexpressTool.js +17 -2
  81. package/dist/tools/AdminexpressTool.js.map +1 -1
  82. package/dist/tools/AltitudeTool.d.ts +35 -44
  83. package/dist/tools/AltitudeTool.js +19 -8
  84. package/dist/tools/AltitudeTool.js.map +1 -1
  85. package/dist/tools/AssietteSupTool.d.ts +51 -34
  86. package/dist/tools/AssietteSupTool.js +17 -2
  87. package/dist/tools/AssietteSupTool.js.map +1 -1
  88. package/dist/tools/BaseTool.d.ts +17 -0
  89. package/dist/tools/BaseTool.js +41 -0
  90. package/dist/tools/BaseTool.js.map +1 -0
  91. package/dist/tools/CadastreTool.d.ts +53 -33
  92. package/dist/tools/CadastreTool.js +17 -2
  93. package/dist/tools/CadastreTool.js.map +1 -1
  94. package/dist/tools/GeocodeTool.d.ts +53 -37
  95. package/dist/tools/GeocodeTool.js +17 -2
  96. package/dist/tools/GeocodeTool.js.map +1 -1
  97. package/dist/tools/GpfWfsDescribeTypeTool.d.ts +66 -94
  98. package/dist/tools/GpfWfsDescribeTypeTool.js +25 -14
  99. package/dist/tools/GpfWfsDescribeTypeTool.js.map +1 -1
  100. package/dist/tools/GpfWfsGetFeatureByIdTool.d.ts +52 -73
  101. package/dist/tools/GpfWfsGetFeatureByIdTool.js +50 -107
  102. package/dist/tools/GpfWfsGetFeatureByIdTool.js.map +1 -1
  103. package/dist/tools/GpfWfsGetFeaturesTool.d.ts +89 -114
  104. package/dist/tools/GpfWfsGetFeaturesTool.js +29 -120
  105. package/dist/tools/GpfWfsGetFeaturesTool.js.map +1 -1
  106. package/dist/tools/GpfWfsSearchTypesTool.d.ts +41 -32
  107. package/dist/tools/GpfWfsSearchTypesTool.js +18 -3
  108. package/dist/tools/GpfWfsSearchTypesTool.js.map +1 -1
  109. package/dist/tools/UrbanismeTool.d.ts +42 -33
  110. package/dist/tools/UrbanismeTool.js +17 -2
  111. package/dist/tools/UrbanismeTool.js.map +1 -1
  112. package/package.json +51 -24
  113. package/dist/gpf/wfs.js.map +0 -1
  114. package/dist/helpers/wfs.d.ts +0 -27
  115. package/dist/helpers/wfs.js +0 -55
  116. package/dist/helpers/wfs.js.map +0 -1
  117. package/dist/helpers/wfs_internal/compile.d.ts +0 -55
  118. package/dist/helpers/wfs_internal/compile.js +0 -596
  119. package/dist/helpers/wfs_internal/compile.js.map +0 -1
  120. package/dist/helpers/wfs_internal/request.js.map +0 -1
  121. package/dist/helpers/wfs_internal/response.d.ts +0 -29
  122. package/dist/helpers/wfs_internal/response.js +0 -59
  123. package/dist/helpers/wfs_internal/response.js.map +0 -1
  124. package/dist/helpers/wfs_internal/schema.d.ts +0 -167
  125. package/dist/helpers/wfs_internal/schema.js.map +0 -1
  126. /package/dist/gpf/{wfs.d.ts → wfs-schema-catalog.d.ts} +0 -0
@@ -1,10 +1,70 @@
1
- export function parseJsonResponse(res: any): Promise<any>;
2
1
  /**
3
- * Fetches and parses a JSON response from a URL
4
- * TODO : Add a timeout
2
+ * Shared HTTP transport and response parsing helpers used across GeoContext services.
5
3
  *
6
- * @param {string} url
7
- * @returns {Promise<any>}
4
+ * Responsibilities:
5
+ * - centralize GET/POST fetch wrappers (`fetchJSONGet`, `fetchJSONPost`);
6
+ * - normalize HTTP, XML and JSON upstream failures as `ServiceResponseError`;
7
+ * - extract known structured error payloads (OGC/WFS XML, GeoServer/altimetry/autocompletion JSON);
8
+ * - keep parser diagnostics explicit for malformed or unsupported payloads.
9
+ *
10
+ * Organization:
11
+ * - this file is intentionally ordered by runtime call flow (top-down),
12
+ * from public entry points to lower-level parsing/scalar helpers.
13
+ */
14
+ type RequestHeaders = Record<string, string>;
15
+ type ResponseHeadersLike = {
16
+ get(name: string): string | null;
17
+ };
18
+ type ResponseLike = {
19
+ status: number;
20
+ statusText: string;
21
+ ok: boolean;
22
+ headers: ResponseHeadersLike;
23
+ text(): Promise<string>;
24
+ };
25
+ export type JsonFetcher<T> = (url: string) => Promise<T>;
26
+ type ServiceResponseErrorOptions = {
27
+ http: {
28
+ status: number;
29
+ statusText?: string;
30
+ };
31
+ service?: {
32
+ code?: string;
33
+ detail?: string;
34
+ };
35
+ };
36
+ /**
37
+ * Structured service error enriched with HTTP and upstream service metadata.
38
+ */
39
+ export declare class ServiceResponseError extends Error {
40
+ httpStatus?: number;
41
+ httpStatusText?: string;
42
+ serviceCode?: string;
43
+ serviceDetail?: string;
44
+ constructor(message: string, options: ServiceResponseErrorOptions);
45
+ }
46
+ /**
47
+ * Sends a GET request expected to return JSON and parses the response.
48
+ *
49
+ * @param url Target URL.
50
+ * @returns The parsed JSON payload.
51
+ */
52
+ export declare function fetchJSONGet(url: string): Promise<any>;
53
+ /**
54
+ * Sends a POST request expected to return JSON and parses the response.
55
+ *
56
+ * @param url Target URL.
57
+ * @param body Optional encoded request body.
58
+ * @param headers Additional request headers.
59
+ * @returns The parsed JSON payload.
60
+ */
61
+ export declare function fetchJSONPost(url: string, body?: string, headers?: RequestHeaders): Promise<any>;
62
+ /**
63
+ * Parses an HTTP response expected to contain JSON and upgrades recognizable
64
+ * XML/JSON service errors to richer structured exceptions.
65
+ *
66
+ * @param res HTTP-like response object returned by `fetch`.
67
+ * @returns The parsed JSON payload.
8
68
  */
9
- export function fetchJSON(url: string): Promise<any>;
10
- export function fetchJSONPost(url: any, body?: string, headers?: {}): Promise<any>;
69
+ export declare function parseJsonResponse(res: ResponseLike): Promise<any>;
70
+ export {};
@@ -1,21 +1,244 @@
1
- import fetch from 'node-fetch';
2
- import { parseXml, XmlElement } from '@rgrove/parse-xml';
1
+ /**
2
+ * Shared HTTP transport and response parsing helpers used across GeoContext services.
3
+ *
4
+ * Responsibilities:
5
+ * - centralize GET/POST fetch wrappers (`fetchJSONGet`, `fetchJSONPost`);
6
+ * - normalize HTTP, XML and JSON upstream failures as `ServiceResponseError`;
7
+ * - extract known structured error payloads (OGC/WFS XML, GeoServer/altimetry/autocompletion JSON);
8
+ * - keep parser diagnostics explicit for malformed or unsupported payloads.
9
+ *
10
+ * Organization:
11
+ * - this file is intentionally ordered by runtime call flow (top-down),
12
+ * from public entry points to lower-level parsing/scalar helpers.
13
+ */
14
+ import fetch from "node-fetch";
15
+ import { parseXml, XmlElement } from "@rgrove/parse-xml";
3
16
  import logger from "../logger.js";
4
- import { HttpsProxyAgent } from 'https-proxy-agent';
17
+ // --- Shared Fetch State ---
18
+ const USER_AGENT_ENV = "USER_AGENT";
19
+ function resolveDefaultUserAgent() {
20
+ const configuredUserAgent = process.env[USER_AGENT_ENV]?.trim();
21
+ return configuredUserAgent || "geocontext";
22
+ }
23
+ const defaultHeaders = new Headers({
24
+ Accept: "application/json",
25
+ "User-Agent": resolveDefaultUserAgent(),
26
+ });
5
27
  const fetchOpts = {
6
- headers: new Headers({
7
- "Accept": "application/json",
8
- "User-Agent": "geocontext"
9
- })
28
+ headers: defaultHeaders,
10
29
  };
11
- if (process.env.HTTP_PROXY) {
12
- fetchOpts.agent = new HttpsProxyAgent(process.env.HTTP_PROXY);
30
+ // --- Service Errors ---
31
+ /**
32
+ * Structured service error enriched with HTTP and upstream service metadata.
33
+ */
34
+ export class ServiceResponseError extends Error {
35
+ httpStatus;
36
+ httpStatusText;
37
+ serviceCode;
38
+ serviceDetail;
39
+ constructor(message, options) {
40
+ super(message);
41
+ this.name = "ServiceResponseError";
42
+ this.httpStatus = options.http.status;
43
+ this.httpStatusText = options.http.statusText;
44
+ this.serviceCode = options.service?.code;
45
+ this.serviceDetail = options.service?.detail;
46
+ }
13
47
  }
14
- function getChild(element, localName) {
15
- return element.children.find((child) => child instanceof XmlElement && child.name.split(":").pop() === localName) || null;
48
+ // --- Constants ---
49
+ const UNKNOWN_UPSTREAM_DETAIL = "Erreur amont inconnue.";
50
+ const JSON_EXCEPTION_CODE_FIELDS = ["code", "exceptionCode"];
51
+ const JSON_EXCEPTION_DETAIL_FIELDS = ["text", "message", "detail"];
52
+ const JSON_NESTED_ERROR_DETAIL_FIELDS = ["description", "message", "detail"];
53
+ const JSON_ROOT_DETAIL_FIELDS = ["message", "detail"];
54
+ const DEFAULT_HTTP_TIMEOUT_SECONDS = 15;
55
+ const UPSTREAM_TIMEOUT_STATUS = 504;
56
+ const UPSTREAM_TIMEOUT_STATUS_TEXT = "Gateway Timeout";
57
+ const UPSTREAM_TIMEOUT_CODE = "TIMEOUT";
58
+ // --- Transport Functions ---
59
+ /**
60
+ * Sends a GET request expected to return JSON and parses the response.
61
+ *
62
+ * @param url Target URL.
63
+ * @returns The parsed JSON payload.
64
+ */
65
+ export async function fetchJSONGet(url) {
66
+ logger.info(`[HTTP] GET ${url} ...`);
67
+ const result = await fetchJSONWithTimeout(url, fetchOpts);
68
+ logger.debug(`[HTTP] GET ${url} : ${JSON.stringify(result)}`);
69
+ return result;
70
+ }
71
+ /**
72
+ * Sends a POST request expected to return JSON and parses the response.
73
+ *
74
+ * @param url Target URL.
75
+ * @param body Optional encoded request body.
76
+ * @param headers Additional request headers.
77
+ * @returns The parsed JSON payload.
78
+ */
79
+ export async function fetchJSONPost(url, body = "", headers = {}) {
80
+ logger.info(`[HTTP] POST ${url} ...`);
81
+ const result = await fetchJSONWithTimeout(url, buildFetchOptions("POST", body, headers));
82
+ logger.debug(`[HTTP] POST ${url} : ${JSON.stringify(result)}`);
83
+ return result;
16
84
  }
17
- // Tente d'extraire un message d'erreur d'une réponse XML de type OGC WFS
18
- function extractXmlError(text) {
85
+ /**
86
+ * Executes a fetch request with a bounded timeout so MCP tool calls do not hang
87
+ * indefinitely when upstream GPF services stop responding.
88
+ *
89
+ * @param url Target URL.
90
+ * @param options Request options.
91
+ * @returns The parsed JSON payload.
92
+ */
93
+ async function fetchJSONWithTimeout(url, options) {
94
+ const timeoutMs = getHttpTimeoutMs();
95
+ const controller = new AbortController();
96
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
97
+ try {
98
+ return await fetch(url, {
99
+ ...options,
100
+ signal: controller.signal,
101
+ }).then(parseJsonResponse);
102
+ }
103
+ catch (error) {
104
+ if (error instanceof Error && error.name === "AbortError") {
105
+ throw new ServiceResponseError(`Délai d'attente dépassé pour le service distant (${timeoutMs} ms).`, {
106
+ http: {
107
+ status: UPSTREAM_TIMEOUT_STATUS,
108
+ statusText: UPSTREAM_TIMEOUT_STATUS_TEXT,
109
+ },
110
+ service: {
111
+ code: UPSTREAM_TIMEOUT_CODE,
112
+ detail: `Le service distant n'a pas répondu dans le délai imparti (${timeoutMs} ms).`,
113
+ },
114
+ });
115
+ }
116
+ throw error;
117
+ }
118
+ finally {
119
+ clearTimeout(timeoutId);
120
+ }
121
+ }
122
+ /**
123
+ * Builds the fetch options used by the shared fetchJSONPost helper.
124
+ * Inherits shared transport settings from `fetchOpts`,
125
+ * merges `defaultHeaders` with caller-provided headers, and only adds `body`
126
+ * when it is explicitly provided.
127
+ *
128
+ * @param method HTTP method to use.
129
+ * @param body Optional request body.
130
+ * @param headers Additional request headers.
131
+ * @returns A `fetch` options object merged with shared defaults.
132
+ */
133
+ function buildFetchOptions(method, body, headers = {}) {
134
+ return {
135
+ ...fetchOpts,
136
+ method,
137
+ headers: new Headers({
138
+ ...Object.fromEntries(defaultHeaders.entries()),
139
+ ...(headers || {}),
140
+ }),
141
+ ...(body !== undefined ? { body } : {}),
142
+ };
143
+ }
144
+ /**
145
+ * Reads and validates the shared upstream timeout configuration.
146
+ *
147
+ * @returns Timeout in milliseconds.
148
+ */
149
+ function getHttpTimeoutMs() {
150
+ const rawTimeoutSeconds = process.env.HTTP_TIMEOUT?.trim();
151
+ if (!rawTimeoutSeconds) {
152
+ return DEFAULT_HTTP_TIMEOUT_SECONDS * 1000;
153
+ }
154
+ const timeoutSeconds = Number(rawTimeoutSeconds);
155
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
156
+ throw new Error(`HTTP_TIMEOUT must be a positive number of seconds. Received: ${rawTimeoutSeconds}`);
157
+ }
158
+ return timeoutSeconds * 1000;
159
+ }
160
+ // --- Response Parsing ---
161
+ /**
162
+ * Parses an HTTP response expected to contain JSON and upgrades recognizable
163
+ * XML/JSON service errors to richer structured exceptions.
164
+ *
165
+ * @param res HTTP-like response object returned by `fetch`.
166
+ * @returns The parsed JSON payload.
167
+ */
168
+ export async function parseJsonResponse(res) {
169
+ const contentType = (res.headers.get("content-type") ?? "").toLowerCase();
170
+ const text = await res.text();
171
+ const context = buildResponseContext(res, text, contentType);
172
+ if (context.text.trim() === "") {
173
+ throw new Error(`Réponse vide du service (${context.responseLabel})`);
174
+ }
175
+ // handleXmlResponse always throws, either with a structured service error or an explicit parsing error.
176
+ if (context.looksLikeXml) {
177
+ handleXmlResponse(context);
178
+ }
179
+ const json = parseJsonBody(context);
180
+ // If the response is not OK, try to extract structured error details from the JSON body
181
+ // and throw a ServiceResponseError with as much context as possible.
182
+ if (!context.isOk) {
183
+ const serviceError = extractJsonServiceError(json);
184
+ throw buildServiceResponseError(context, `Erreur HTTP du service (${context.responseLabel}): ${serviceError.detail}`, {
185
+ code: serviceError.code,
186
+ detail: serviceError.detail,
187
+ });
188
+ }
189
+ return json;
190
+ }
191
+ /**
192
+ * Parses a JSON payload or throws a stable parsing error.
193
+ *
194
+ * @param context Normalized response context.
195
+ * @returns Parsed JSON payload.
196
+ */
197
+ function parseJsonBody(context) {
198
+ try {
199
+ return JSON.parse(context.text);
200
+ }
201
+ catch {
202
+ const details = buildBodyDetails(context.contentType, context.text);
203
+ throw new Error(`Réponse JSON invalide du service (${context.responseLabel}, ${details.join(", ")})`);
204
+ }
205
+ }
206
+ // --- XML Error Extraction ---
207
+ /**
208
+ * Handles XML-like responses and either throws structured upstream errors
209
+ * or explicit parsing diagnostics.
210
+ *
211
+ * @param context Normalized response context.
212
+ * @throws {ServiceResponseError | Error} Always throws for XML-like responses.
213
+ */
214
+ function handleXmlResponse(context) {
215
+ const xmlError = extractXmlServiceError(context.text);
216
+ if (xmlError) {
217
+ if (!context.isOk) {
218
+ throw buildServiceResponseError(context, `Erreur HTTP du service (${context.responseLabel}): ${xmlError.message}`, {
219
+ code: xmlError.code,
220
+ detail: xmlError.detail,
221
+ });
222
+ }
223
+ throw buildServiceResponseError(context, xmlError.message, {
224
+ code: xmlError.code,
225
+ detail: xmlError.detail,
226
+ });
227
+ }
228
+ const unstructuredDetail = previewBody(context.text) || UNKNOWN_UPSTREAM_DETAIL;
229
+ if (!context.isOk) {
230
+ throw buildServiceResponseError(context, `Erreur HTTP du service (${context.responseLabel}): ${unstructuredDetail}`, { detail: unstructuredDetail });
231
+ }
232
+ const details = buildBodyDetails(context.contentType, context.text);
233
+ throw new Error(`Réponse XML non exploitable du service (${context.responseLabel}, ${details.join(", ")})`);
234
+ }
235
+ /**
236
+ * Tries to extract an OGC/WFS-style service error from an XML response body.
237
+ *
238
+ * @param text Raw XML response body.
239
+ * @returns A parsed service error payload, or `null` when the XML payload is not a recognized error report.
240
+ */
241
+ function extractXmlServiceError(text) {
19
242
  try {
20
243
  const root = parseXml(text).children.find((child) => child instanceof XmlElement);
21
244
  const rootName = root?.name.split(":").pop();
@@ -29,12 +252,145 @@ function extractXmlError(text) {
29
252
  const code = exception.attributes?.exceptionCode || exception.attributes?.code;
30
253
  const message = getChild(exception, "ExceptionText")?.text?.trim() || exception.text?.trim() || "";
31
254
  const errorMessage = [code, message].filter(Boolean).join(": ");
32
- return errorMessage ? new Error(errorMessage) : null;
255
+ return errorMessage
256
+ ? {
257
+ code,
258
+ detail: message || undefined,
259
+ message: errorMessage,
260
+ }
261
+ : null;
33
262
  }
34
263
  catch {
35
264
  return null;
36
265
  }
37
266
  }
267
+ /**
268
+ * Returns the first child XML element matching the requested local name.
269
+ *
270
+ * @param element Parent element to inspect.
271
+ * @param localName Local XML name without namespace prefix.
272
+ * @returns The matching child element, or `null` when none is found.
273
+ */
274
+ function getChild(element, localName) {
275
+ if (!element) {
276
+ return null;
277
+ }
278
+ const child = element.children.find((candidate) => candidate instanceof XmlElement &&
279
+ candidate.name.split(":").pop() === localName);
280
+ return child ?? null;
281
+ }
282
+ // --- JSON Error Extraction ---
283
+ /**
284
+ * Extracts structured upstream error details from a parsed JSON error body.
285
+ *
286
+ * @param json Parsed JSON payload.
287
+ * @returns A normalized `{ code, detail }` pair.
288
+ */
289
+ function extractJsonServiceError(json) {
290
+ if (typeof json === "string") {
291
+ return { detail: asNonEmptyString(json) ?? UNKNOWN_UPSTREAM_DETAIL };
292
+ }
293
+ const rootRecord = asRecord(json) ?? {};
294
+ const nestedError = asRecord(rootRecord.error);
295
+ const firstException = firstRecordItem(rootRecord.exceptions);
296
+ const rootDetailItem = firstStringItem(rootRecord.detail);
297
+ const nestedErrorDetailItem = firstStringItem(nestedError?.detail);
298
+ const rootCode = asErrorCode(rootRecord.code);
299
+ const nestedErrorCode = asErrorCode(nestedError?.code);
300
+ const code = pickFirstString([
301
+ pickFirstStringField(firstException, JSON_EXCEPTION_CODE_FIELDS),
302
+ rootCode,
303
+ nestedErrorCode,
304
+ ]);
305
+ const detail = pickFirstString([
306
+ pickFirstStringField(firstException, JSON_EXCEPTION_DETAIL_FIELDS),
307
+ pickFirstStringField(nestedError, JSON_NESTED_ERROR_DETAIL_FIELDS),
308
+ nestedErrorDetailItem,
309
+ rootDetailItem,
310
+ pickFirstStringField(rootRecord, JSON_ROOT_DETAIL_FIELDS),
311
+ typeof rootRecord.error === "string" ? rootRecord.error : undefined,
312
+ ]) || previewBody(JSON.stringify(json)) || UNKNOWN_UPSTREAM_DETAIL;
313
+ return { code, detail };
314
+ }
315
+ // --- Context Builders ---
316
+ /**
317
+ * Builds a normalized response context consumed by parsing helpers.
318
+ *
319
+ * @param res HTTP-like response object.
320
+ * @param text Raw response body.
321
+ * @param contentType Normalized content-type header.
322
+ * @returns Structured context used by XML/JSON parsers.
323
+ */
324
+ function buildResponseContext(res, text, contentType) {
325
+ return {
326
+ text,
327
+ contentType,
328
+ looksLikeXml: isLikelyXml(contentType, text),
329
+ responseLabel: buildResponseLabel(res.status, res.statusText),
330
+ isOk: res.ok,
331
+ status: res.status,
332
+ };
333
+ }
334
+ /**
335
+ * Checks whether a response likely contains XML data.
336
+ *
337
+ * TODO: Use a less stupid heuristic. Ok for a proof of concept but should be improved for production use.
338
+ *
339
+ * @param contentType Normalized content-type header.
340
+ * @param text Raw response body.
341
+ * @returns `true` when XML parsing should be attempted.
342
+ */
343
+ function isLikelyXml(contentType, text) {
344
+ return contentType.includes("xml") || text.trim().startsWith("<");
345
+ }
346
+ /**
347
+ * Builds a structured service response error tied to the current HTTP context.
348
+ *
349
+ * @param context Normalized response context.
350
+ * @param message Error message.
351
+ * @param service Optional upstream service metadata.
352
+ * @returns A normalized `ServiceResponseError`.
353
+ */
354
+ function buildServiceResponseError(context, message, service) {
355
+ return new ServiceResponseError(message, {
356
+ http: {
357
+ status: context.status,
358
+ statusText: context.responseLabel,
359
+ },
360
+ ...(service ? { service } : {}),
361
+ });
362
+ }
363
+ /**
364
+ * Builds reusable diagnostic details for parser errors.
365
+ *
366
+ * @param contentType Normalized content-type header.
367
+ * @param text Raw response body.
368
+ * @returns A detail string list used in human-readable error messages.
369
+ */
370
+ function buildBodyDetails(contentType, text) {
371
+ const details = [`content-type=${contentType || "inconnu"}`];
372
+ const bodyPreview = previewBody(text);
373
+ if (bodyPreview) {
374
+ details.push(`extrait=${bodyPreview}`);
375
+ }
376
+ return details;
377
+ }
378
+ /**
379
+ * Builds a human-readable response label combining HTTP status and text.
380
+ *
381
+ * @param status HTTP status code.
382
+ * @param statusText HTTP status text.
383
+ * @returns A label such as `400 Bad Request`.
384
+ */
385
+ function buildResponseLabel(status, statusText) {
386
+ return [status, statusText].filter(Boolean).join(" ") || "réponse HTTP";
387
+ }
388
+ /**
389
+ * Returns a short single-line preview of a response body for diagnostics.
390
+ *
391
+ * @param text Raw response body.
392
+ * @returns A trimmed preview limited to 200 characters.
393
+ */
38
394
  function previewBody(text) {
39
395
  const trimmed = text.trim();
40
396
  if (!trimmed) {
@@ -42,85 +398,103 @@ function previewBody(text) {
42
398
  }
43
399
  return trimmed.replace(/\s+/g, " ").slice(0, 200);
44
400
  }
45
- export async function parseJsonResponse(res) {
46
- const contentType = (res.headers?.get?.("content-type") || "").toLowerCase();
47
- const text = await res.text();
48
- const looksLikeXml = contentType.includes("xml") || text.trim().startsWith("<");
49
- const responseLabel = [res.status, res.statusText].filter(Boolean).join(" ") || "réponse HTTP";
50
- const hasValidStatus = Number.isFinite(res.status);
51
- const isOk = typeof res.ok === "boolean"
52
- ? res.ok
53
- : hasValidStatus && res.status >= 200 && res.status < 300;
54
- if (text.trim() === "") {
55
- throw new Error(`Réponse vide du service (${responseLabel})`);
56
- }
57
- if (looksLikeXml) {
58
- const xmlError = extractXmlError(text);
59
- if (xmlError) {
60
- if (!isOk) {
61
- throw new Error(`Erreur HTTP du service (${responseLabel}): ${xmlError.message}`);
62
- }
63
- throw xmlError;
64
- }
65
- const details = [`content-type=${contentType || "inconnu"}`];
66
- const bodyPreview = previewBody(text);
67
- if (bodyPreview) {
68
- details.push(`extrait=${bodyPreview}`);
69
- }
70
- throw new Error(`Réponse XML non exploitable du service (${responseLabel}, ${details.join(", ")})`);
401
+ // --- Scalar Helpers ---
402
+ /**
403
+ * Converts an unknown scalar code to a normalized string.
404
+ *
405
+ * @param value Unknown code value.
406
+ * @returns A non-empty string representation, or `undefined`.
407
+ */
408
+ function asErrorCode(value) {
409
+ if (typeof value === "string") {
410
+ return asNonEmptyString(value);
71
411
  }
72
- let json;
73
- try {
74
- json = JSON.parse(text);
412
+ if (typeof value === "number" && Number.isFinite(value)) {
413
+ return String(value);
75
414
  }
76
- catch {
77
- const details = [`content-type=${contentType || "inconnu"}`];
78
- const bodyPreview = previewBody(text);
79
- if (bodyPreview) {
80
- details.push(`extrait=${bodyPreview}`);
415
+ return undefined;
416
+ }
417
+ /**
418
+ * Returns the first non-empty string from an array-like unknown value.
419
+ *
420
+ * @param value Unknown value to inspect.
421
+ * @returns The first non-empty string item, or `undefined`.
422
+ */
423
+ function firstStringItem(value) {
424
+ if (!Array.isArray(value)) {
425
+ return undefined;
426
+ }
427
+ return pickFirstString(value);
428
+ }
429
+ /**
430
+ * Returns the first non-empty string for a record among ordered field names.
431
+ *
432
+ * @param record Object-like value to inspect.
433
+ * @param fields Ordered field names.
434
+ * @returns The first non-empty string value, or `undefined`.
435
+ */
436
+ function pickFirstStringField(record, fields) {
437
+ if (!record) {
438
+ return undefined;
439
+ }
440
+ return pickFirstString(fields.map((field) => record[field]));
441
+ }
442
+ /**
443
+ * Returns the first non-empty string in the provided values.
444
+ *
445
+ * @param values Candidate values ordered by priority.
446
+ * @returns The first normalized non-empty string, or `undefined`.
447
+ */
448
+ function pickFirstString(values) {
449
+ for (const value of values) {
450
+ const normalized = asNonEmptyString(value);
451
+ if (normalized) {
452
+ return normalized;
81
453
  }
82
- throw new Error(`Réponse JSON invalide du service (${responseLabel}, ${details.join(", ")})`);
83
- }
84
- if (!isOk) {
85
- const errorMessage = json?.message
86
- || json?.error
87
- || json?.errorMessage
88
- || json?.msg
89
- || json?.title
90
- || json?.detail
91
- || (typeof json === "string" ? json : previewBody(JSON.stringify(json)));
92
- throw new Error(`Erreur HTTP du service (${responseLabel}): ${errorMessage}`);
93
454
  }
94
- return json;
455
+ return undefined;
95
456
  }
96
457
  /**
97
- * Fetches and parses a JSON response from a URL
98
- * TODO : Add a timeout
458
+ * Returns the first object item from an unknown array value.
99
459
  *
100
- * @param {string} url
101
- * @returns {Promise<any>}
460
+ * @param value Unknown value to inspect.
461
+ * @returns First object item as a record, or `undefined`.
102
462
  */
103
- export async function fetchJSON(url) {
104
- logger.info(`[HTTP] GET ${url} ...`);
105
- const result = await fetch(url, fetchOpts).then(parseJsonResponse);
106
- logger.debug(`[HTTP] GET ${url} : ${JSON.stringify(result)}`);
107
- return result;
463
+ function firstRecordItem(value) {
464
+ if (!Array.isArray(value)) {
465
+ return undefined;
466
+ }
467
+ for (const item of value) {
468
+ const record = asRecord(item);
469
+ if (record) {
470
+ return record;
471
+ }
472
+ }
473
+ return undefined;
108
474
  }
109
- function buildFetchOptions(method, body, headers) {
110
- return {
111
- ...fetchOpts,
112
- method,
113
- headers: new Headers({
114
- ...Object.fromEntries(fetchOpts.headers.entries()),
115
- ...(headers || {})
116
- }),
117
- ...(body !== undefined ? { body } : {})
118
- };
475
+ /**
476
+ * Returns a plain record when the provided value is an object.
477
+ *
478
+ * @param value Unknown value to inspect.
479
+ * @returns A record view of the object, or `undefined`.
480
+ */
481
+ function asRecord(value) {
482
+ if (!value || typeof value !== "object") {
483
+ return undefined;
484
+ }
485
+ return value;
119
486
  }
120
- export async function fetchJSONPost(url, body = "", headers = {}) {
121
- logger.info(`[HTTP] POST ${url} ...`);
122
- const result = await fetch(url, buildFetchOptions("POST", body, headers)).then(parseJsonResponse);
123
- logger.debug(`[HTTP] POST ${url} : ${JSON.stringify(result)}`);
124
- return result;
487
+ /**
488
+ * Returns a trimmed string when the provided value is a non-empty string.
489
+ *
490
+ * @param value Unknown value to inspect.
491
+ * @returns A normalized string or `undefined`.
492
+ */
493
+ function asNonEmptyString(value) {
494
+ if (typeof value !== "string") {
495
+ return undefined;
496
+ }
497
+ const trimmed = value.trim();
498
+ return trimmed.length > 0 ? trimmed : undefined;
125
499
  }
126
500
  //# sourceMappingURL=http.js.map