france-data-mcp 0.10.6 → 0.10.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/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +145 -45
- package/dist/index.js.map +1 -1
- package/dist/sante/index.js +56 -32
- package/dist/sante/index.js.map +1 -1
- package/dist/territoire/index.d.ts +29 -7
- package/dist/territoire/index.js +97 -15
- package/dist/territoire/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":"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","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.10.6\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/version.ts","../bin/cli.ts"],"names":[],"mappings":";;;;;;;AASO,IAAM,OAAA,GAAU,QAAA;;;ACqBvB,IAAM,gBAAA,GAAmB,wCAAA;AACzB,IAAM,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,mBAAA,IAAuB;AACpD,IAAM,UAAA,GAAa,uBAAuB,OAAO,CAAA;AACjD,IAAM,uBAAA,GAA0B,MAAA;AAEhC,IAAM,aAAA,GAAgB,QAAQ,GAAA,CAAI,0BAAA;AAClC,IAAM,aAAA,GAAgB,OAAO,aAAa,CAAA;AAC1C,IAAM,cAAA,GAAiB,MAAA,CAAO,QAAA,CAAS,aAAa,KAAK,aAAA,GAAgB,CAAA;AACzE,IAAM,kBAAA,GAAqB,iBAAiB,aAAA,GAAgB,GAAA;AAG5D,IAAI,aAAA,KAAkB,MAAA,IAAa,CAAC,cAAA,EAAgB;AAClD,EAAA,OAAA,CAAQ,KAAA;AAAA,IACN,CAAA,kDAAA,EAAqD,aAAa,CAAA,qBAAA,EAAwB,kBAAkB,CAAA,EAAA;AAAA,GAC9G;AACF;AASA,SAAS,QAAQ,IAAA,EAAyB;AACxC,EAAA,IAAI;AACF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAC3B,IAAA,IAAI,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,IAAY,QAAQ,GAAA,EAAK;AACjD,MAAA,MAAM,KAAM,GAAA,CAAuB,EAAA;AACnC,MAAA,IAAI,OAAO,OAAO,QAAA,IAAY,OAAO,OAAO,QAAA,IAAY,EAAA,KAAO,MAAM,OAAO,EAAA;AAAA,IAC9E;AAAA,EACF,CAAA,CAAA,MAAQ;AAAA,EAIR;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAM,eAAA,GAAkB,CAAC,CAAA,KAAoB;AAC3C,EAAA,MAAA,CAAO,MAAM,CAAC,CAAA;AAChB,CAAA;AAMA,SAAS,gBAAA,CACP,EAAA,EACA,IAAA,EACA,OAAA,EACA,QAAA,EACM;AACN,EAAA,MAAM,OAAA,GAAU,EAAE,OAAA,EAAS,KAAA,EAAO,IAAI,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAQ,EAAE;AAC/D,EAAA,QAAA,CAAS,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC;AAAA,CAAI,CAAA;AACzC;AAQA,eAAsB,WAAA,CACpB,IAAA,EACA,OAAA,GAAwB,KAAA,EACxB,WAAgC,eAAA,EACjB;AACf,EAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,EAAA,IAAI,CAAC,OAAA,EAAS;AAGd,EAAA,MAAM,EAAA,GAAK,QAAQ,OAAO,CAAA;AAE1B,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI;AACF,IAAA,QAAA,GAAW,MAAM,QAAQ,QAAA,EAAU;AAAA,MACjC,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACP,cAAA,EAAgB,kBAAA;AAAA,QAChB,YAAA,EAAc,UAAA;AAAA,QACd,MAAA,EAAQ;AAAA,OACV;AAAA,MACA,IAAA,EAAM,OAAA;AAAA,MACN,MAAA,EAAQ,WAAA,CAAY,OAAA,CAAQ,kBAAkB;AAAA,KAC/C,CAAA;AAAA,EACH,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,MAAA,GAAS,eAAe,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAC9E,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,sCAAA,EAAyC,MAAM,CAAA,CAAE,CAAA;AAC/D,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,4BAAA,EAA+B,aAAa,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA;AAAA,MACvD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,MAAM,SAAS,IAAA,EAAK;AAAA,EAC7B,SAAS,GAAA,EAAK;AAKZ,IAAA,MAAM,MAAA,GAAS,eAAe,GAAA,YAAe,KAAA,GAAQ,IAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA;AAC9E,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,+CAAA,EAAkD,MAAM,CAAA,CAAE,CAAA;AACxE,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,6BAAA,EAAgC,aAAa,CAAA,EAAA,EAAK,MAAM,CAAA,CAAA;AAAA,MACxD;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAEA,EAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,IAAA,gBAAA;AAAA,MACE,EAAA;AAAA,MACA,uBAAA;AAAA,MACA,CAAA,cAAA,EAAiB,QAAA,CAAS,MAAM,CAAA,MAAA,EAAS,aAAa,CAAA,EAAA,EAAK,cAAA,CAAe,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,GAAG,CAAC,CAAC,CAAA,CAAA;AAAA,MAC7F;AAAA,KACF;AACA,IAAA;AAAA,EACF;AAKA,EAAA,IAAI,IAAA,CAAK,SAAS,CAAA,EAAG,QAAA,CAAS,GAAG,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAC;AAAA,CAAI,CAAA;AAChE;AAeA,SAAS,mBAAmB,QAAA,EAA0B;AACpD,EAAA,IAAI;AACF,IAAA,MAAM,CAAA,GAAI,IAAI,GAAA,CAAI,QAAQ,CAAA;AAC1B,IAAA,CAAA,CAAE,QAAA,GAAW,EAAA;AACb,IAAA,CAAA,CAAE,QAAA,GAAW,EAAA;AACb,IAAA,OAAO,EAAE,QAAA,EAAS;AAAA,EACpB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,QAAA;AAAA,EACT;AACF;AAIA,IAAM,aAAA,GAAgB,mBAAmB,QAAQ,CAAA;AAUjD,SAAS,eAAe,MAAA,EAAwB;AAC9C,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,2BAAA,EAA6B,eAAe,CAAA;AACpE;AAEA,eAAe,IAAA,GAAsB;AACnC,EAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,uBAAA,EAA0B,OAAO,CAAA,QAAA,EAAM,aAAa,CAAA,CAAE,CAAA;AACpE,EAAA,MAAM,EAAA,GAAK,gBAAgB,EAAE,KAAA,EAAO,OAAO,SAAA,EAAW,MAAA,CAAO,mBAAmB,CAAA;AAChF,EAAA,WAAA,MAAiB,QAAQ,EAAA,EAAI;AAC3B,IAAA,MAAM,YAAY,IAAI,CAAA;AAAA,EACxB;AACF;AAcO,SAAS,YAAA,CAAa,eAAuB,KAAA,EAAoC;AACtF,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,KAAA;AACtC,EAAA,IAAI;AACF,IAAA,OAAO,aAAa,aAAA,CAAc,aAAa,CAAC,CAAA,KAAM,aAAa,KAAK,CAAA;AAAA,EAC1E,CAAA,CAAA,MAAQ;AAQN,IAAA,OAAO,KAAA;AAAA,EACT;AACF;AAEA,IAAI,aAAa,MAAA,CAAA,IAAA,CAAY,GAAA,EAAK,QAAQ,IAAA,CAAK,CAAC,CAAC,CAAA,EAAG;AAClD,EAAA,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAAiB;AAC7B,IAAA,MAAM,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC9D,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,6BAAA,EAAgC,MAAM,CAAA,CAAE,CAAA;AACtD,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EAChB,CAAC,CAAA;AACH","file":"cli.js","sourcesContent":["/**\n * Version courante du serveur MCP + wrapper npm. Source de vérité partagée :\n * - `api/mcp.ts` → expose via `initialize.serverInfo.version` au client MCP\n * - `bin/cli.ts` → expose dans le User-Agent HTTP + le banner stderr\n *\n * Synchronisée manuellement avec `package.json.version` à chaque release.\n * Une déclaration en TS pur évite la friction des import attributes JSON\n * (instables entre tsup/esbuild/@vercel/node).\n */\nexport const VERSION = \"0.10.8\";\n","#!/usr/bin/env node\n/**\n * france-data-mcp — wrapper npm.\n *\n * Forwarde le protocole MCP stdio (NDJSON sur stdin/stdout) vers l'endpoint\n * HTTP `france-data-mcp.vercel.app/mcp`. Permet aux clients MCP qui ne savent\n * pas appeler un endpoint HTTP distant (Claude Desktop natif, certains IDE)\n * d'utiliser le serveur via `npx france-data-mcp`.\n *\n * Architecture :\n * - Lit stdin ligne par ligne (NDJSON, spec MCP stdio transport). Trim le\n * whitespace périphérique avant forward — transformation volontaire et\n * inoffensive (n'altère pas le JSON-RPC payload).\n * - Pour chaque ligne non vide, POST vers `ENDPOINT` et écrit la réponse\n * sur stdout (NDJSON).\n * - En cas d'erreur réseau, HTTP >= 400, ou body stream interrompu, émet\n * une réponse JSON-RPC error (-32603) pour ne JAMAIS faire hang le client.\n *\n * stdout doit rester pur JSON-RPC (NDJSON) — tout log interne va sur stderr\n * via `console.error` (jamais `stdout.write` pour autre chose qu'une réponse\n * JSON-RPC). Pas d'état stateful : le serveur HTTP est stateless lui aussi.\n */\n\nimport { realpathSync } from \"node:fs\";\nimport { stdin, stdout } from \"node:process\";\nimport { createInterface } from \"node:readline\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { VERSION } from \"../src/core/version.js\";\n\nconst DEFAULT_ENDPOINT = \"https://france-data-mcp.vercel.app/mcp\";\nconst ENDPOINT = process.env.FRANCE_DATA_MCP_URL || DEFAULT_ENDPOINT;\nconst USER_AGENT = `france-data-mcp-npm/${VERSION}`;\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\nconst rawTimeoutEnv = process.env.FRANCE_DATA_MCP_TIMEOUT_MS;\nconst parsedTimeout = Number(rawTimeoutEnv);\nconst isValidTimeout = Number.isFinite(parsedTimeout) && parsedTimeout > 0;\nconst REQUEST_TIMEOUT_MS = isValidTimeout ? parsedTimeout : 60_000;\n// Signaler le fallback côté stderr (jamais stdout — réservé au JSON-RPC) pour\n// éviter un silent failure si l'utilisateur a tapé une valeur invalide.\nif (rawTimeoutEnv !== undefined && !isValidTimeout) {\n console.error(\n `[france-data-mcp-npm] FRANCE_DATA_MCP_TIMEOUT_MS=\"${rawTimeoutEnv}\" invalide, fallback ${REQUEST_TIMEOUT_MS}ms`,\n );\n}\n\ntype JsonRpcId = string | number | null;\ntype JsonRpcMessage = { id?: JsonRpcId; method?: string };\n\n/**\n * Extrait l'id JSON-RPC d'une ligne (best-effort). Utilisé uniquement pour\n * construire une réponse error propre quand le forward réseau échoue.\n */\nfunction parseId(line: string): JsonRpcId {\n try {\n const msg = JSON.parse(line) as unknown;\n if (msg && typeof msg === \"object\" && \"id\" in msg) {\n const id = (msg as JsonRpcMessage).id;\n if (typeof id === \"string\" || typeof id === \"number\" || id === null) return id;\n }\n } catch {\n // Best-effort : si la ligne n'est pas du JSON valide, forwardLine émettra\n // quand même une réponse JSON-RPC error sur stdout avec id=null. Le\n // diagnostic texte va sur stderr via console.error.\n }\n return null;\n}\n\nconst defaultWriteOut = (s: string): void => {\n stdout.write(s);\n};\n\n/**\n * Émet une réponse JSON-RPC error sur stdout. NDJSON appliqué de façon\n * uniforme (1 message = 1 ligne).\n */\nfunction emitJsonRpcError(\n id: JsonRpcId,\n code: number,\n message: string,\n writeOut: (s: string) => void,\n): void {\n const payload = { jsonrpc: \"2.0\", id, error: { code, message } };\n writeOut(`${JSON.stringify(payload)}\\n`);\n}\n\n/**\n * POST une ligne JSON-RPC vers l'endpoint HTTP et écrit la réponse sur stdout.\n * Catche toutes les erreurs (réseau, timeout, HTTP >=400, stream interrompu)\n * en émettant une réponse JSON-RPC error — le client MCP voit toujours une\n * réponse pour chaque request, jamais de hang.\n */\nexport async function forwardLine(\n line: string,\n fetchFn: typeof fetch = fetch,\n writeOut: (s: string) => void = defaultWriteOut,\n): Promise<void> {\n const trimmed = line.trim();\n if (!trimmed) return;\n\n // Parsing d'id fait UNE seule fois, réutilisé sur tous les chemins d'erreur.\n const id = parseId(trimmed);\n\n let response: Response;\n try {\n response = await fetchFn(ENDPOINT, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/json\",\n \"user-agent\": USER_AGENT,\n accept: \"application/json\",\n },\n body: trimmed,\n signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),\n });\n } catch (err) {\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] forward failed: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Network error forwarding to ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n let text: string;\n try {\n text = await response.text();\n } catch (err) {\n // Stream interrompu après les headers (gateway timeout, réseau coupé) :\n // sans ce catch, la promise rejette → main() crash → client hang sur l'id.\n // console.error pour le diagnostic local, capture Sentry inapplicable\n // côté wrapper client (par design : pas de telemetry).\n const reason = sanitizeReason(err instanceof Error ? err.message : String(err));\n console.error(`[france-data-mcp-npm] body stream interrupted: ${reason}`);\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Body stream interrupted from ${SAFE_ENDPOINT}: ${reason}`,\n writeOut,\n );\n return;\n }\n\n if (!response.ok) {\n emitJsonRpcError(\n id,\n JSON_RPC_INTERNAL_ERROR,\n `Upstream HTTP ${response.status} from ${SAFE_ENDPOINT}: ${sanitizeReason(text.slice(0, 200))}`,\n writeOut,\n );\n return;\n }\n\n // L'endpoint Vercel renvoie un seul objet JSON-RPC (status 200) ou rien\n // (204 pour les notifications). Passthrough verbatim — pas de re-sérialisation\n // pour préserver la précision (ordre des clés, formats numériques).\n if (text.length > 0) writeOut(`${text.replace(/\\n+$/u, \"\")}\\n`);\n}\n\n/**\n * Masque les credentials userinfo d'une URL connue (notre ENDPOINT) avant\n * inclusion dans un log stderr ou un message error JSON-RPC stdout.\n *\n * Surfaces couvertes :\n * 1. Banner stderr au démarrage (`SAFE_ENDPOINT`)\n * 2. Messages error JSON-RPC qui interpolent `${SAFE_ENDPOINT}`\n * 3. Messages error des exceptions `fetch` Node 22+ — leur `err.message`\n * embarque l'URL fautive ENTIÈRE incluant userinfo (\"Request cannot be\n * constructed from a URL that includes credentials: https://user:pass@…\").\n * Cette surface est couverte par `sanitizeReason()` complémentaire — ne PAS\n * se reposer sur cette fonction seule.\n */\nfunction safeEndpointForLog(endpoint: string): string {\n try {\n const u = new URL(endpoint);\n u.username = \"\";\n u.password = \"\";\n return u.toString();\n } catch {\n return endpoint;\n }\n}\n\n// Calculé une seule fois au module load — réutilisé dans tous les messages\n// (banner stderr + 3 chemins d'erreur stdout JSON-RPC).\nconst SAFE_ENDPOINT = safeEndpointForLog(ENDPOINT);\n\n/**\n * Strip toute URL `scheme://user:pass@host` d'une string libre. Le runtime\n * `fetch` Node 22+ throw un `TypeError` dont le message contient verbatim\n * l'URL fautive, incluant userinfo. Sans sanitization, le `reason` d'une\n * erreur réseau fuiterait les credentials côté stdout (JSON-RPC error) ET\n * stderr (diagnostic). Defense-in-depth obligatoire — la même URL pourrait\n * arriver via un message d'erreur de proxy, DNS, gateway, etc.\n */\nfunction sanitizeReason(reason: string): string {\n return reason.replace(/(https?:\\/\\/)[^/\\s@]+@/giu, \"$1[redacted]@\");\n}\n\nasync function main(): Promise<void> {\n console.error(`[france-data-mcp-npm] v${VERSION} → ${SAFE_ENDPOINT}`);\n const rl = createInterface({ input: stdin, crlfDelay: Number.POSITIVE_INFINITY });\n for await (const line of rl) {\n await forwardLine(line);\n }\n}\n\n/**\n * Détecte si le module est exécuté directement (vs importé par un test).\n * Évite de démarrer la boucle stdin pendant les tests.\n *\n * V0.7.6 fix : la garde précédente comparait `pathToFileURL(argv1).href` à\n * `import.meta.url`. Quand npm/npx exécutent le bin via un symlink dans\n * `node_modules/.bin/`, `process.argv[1]` reste le chemin du symlink mais\n * Node ESM résout `import.meta.url` vers la cible réelle. Les deux divergent\n * → `main()` jamais appelé → process exit silencieux. Régression silencieuse\n * en prod depuis V0.7.2 (1er wrapper npm). Fix : comparer les `realpath` des\n * deux côtés pour matcher quel que soit le routage symlink.\n */\nexport function isMainModule(importMetaUrl: string, argv1: string | undefined): boolean {\n if (typeof argv1 !== \"string\") return false;\n try {\n return realpathSync(fileURLToPath(importMetaUrl)) === realpathSync(argv1);\n } catch {\n // Catch volontairement silencieux (PAS un silent failure métier) :\n // c'est une décision booléenne sans effet utilisateur — on ne hand off\n // aucune donnée, on ne masque aucune erreur API. Si realpath ou\n // fileURLToPath throw (URL malformée, fichier inexistant — typique en\n // contexte test/import abstrait), la bonne réponse est \"ne PAS démarrer\n // main()\", pas \"logger une erreur\" qui polluerait stderr de tous les\n // tests qui importent le module.\n return false;\n }\n}\n\nif (isMainModule(import.meta.url, process.argv[1])) {\n main().catch((err: unknown) => {\n const reason = err instanceof Error ? err.message : String(err);\n console.error(`[france-data-mcp-npm] fatal: ${reason}`);\n process.exit(1);\n });\n}\n\nexport { ENDPOINT, USER_AGENT, parseId, safeEndpointForLog };\n"]}
|
package/dist/index.js
CHANGED
|
@@ -74,8 +74,14 @@ async function fetchJson(url, options = {}) {
|
|
|
74
74
|
if (err instanceof HttpError) throw err;
|
|
75
75
|
lastError = err;
|
|
76
76
|
if (lastError instanceof SyntaxError) {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
const isFinalAttempt2 = attempt === maxRetries;
|
|
78
|
+
const log2 = isFinalAttempt2 ? console.error : console.warn;
|
|
79
|
+
log2(
|
|
80
|
+
`[france-data-mcp] invalid JSON response from ${url} (attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message}`
|
|
81
|
+
);
|
|
82
|
+
if (isFinalAttempt2) break;
|
|
83
|
+
await sleep(baseDelayMs * 2 ** attempt + jitter());
|
|
84
|
+
continue;
|
|
79
85
|
}
|
|
80
86
|
if (lastError.name === "AbortError") {
|
|
81
87
|
console.warn(`[france-data-mcp] fetch aborted (caller signal) on ${url}`);
|
|
@@ -216,16 +222,55 @@ function parseLooseNumber(value) {
|
|
|
216
222
|
return Number.isFinite(num) ? num : void 0;
|
|
217
223
|
}
|
|
218
224
|
|
|
225
|
+
// src/core/text-match.ts
|
|
226
|
+
function normalizeForCompare(value) {
|
|
227
|
+
return value.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[.,'’\-\s]+/g, " ").trim();
|
|
228
|
+
}
|
|
229
|
+
function diceCoefficient(a, b) {
|
|
230
|
+
if (a === b) return 1;
|
|
231
|
+
if (a.length < 2 || b.length < 2) return a === b ? 1 : 0;
|
|
232
|
+
const bigramsA = /* @__PURE__ */ new Map();
|
|
233
|
+
for (let i = 0; i < a.length - 1; i++) {
|
|
234
|
+
const bg = a.slice(i, i + 2);
|
|
235
|
+
bigramsA.set(bg, (bigramsA.get(bg) ?? 0) + 1);
|
|
236
|
+
}
|
|
237
|
+
let intersection = 0;
|
|
238
|
+
let totalB = 0;
|
|
239
|
+
for (let i = 0; i < b.length - 1; i++) {
|
|
240
|
+
const bg = b.slice(i, i + 2);
|
|
241
|
+
totalB++;
|
|
242
|
+
const inA = bigramsA.get(bg);
|
|
243
|
+
if (inA !== void 0 && inA > 0) {
|
|
244
|
+
intersection++;
|
|
245
|
+
bigramsA.set(bg, inA - 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const totalA = a.length - 1;
|
|
249
|
+
if (totalA + totalB === 0) return 0;
|
|
250
|
+
return 2 * intersection / (totalA + totalB);
|
|
251
|
+
}
|
|
252
|
+
|
|
219
253
|
// src/territoire/geocode.ts
|
|
220
254
|
var BASE_URL2 = "https://data.geopf.fr/geocodage";
|
|
221
|
-
var
|
|
255
|
+
var LOW_SCORE_THRESHOLD_BY_TYPE = {
|
|
256
|
+
housenumber: 0.7,
|
|
257
|
+
street: 0.6,
|
|
258
|
+
locality: 0.6,
|
|
259
|
+
municipality: 0.5
|
|
260
|
+
};
|
|
261
|
+
var DEFAULT_LOW_SCORE_THRESHOLD = 0.5;
|
|
262
|
+
function lowScoreThreshold(type) {
|
|
263
|
+
return LOW_SCORE_THRESHOLD_BY_TYPE[type] ?? DEFAULT_LOW_SCORE_THRESHOLD;
|
|
264
|
+
}
|
|
265
|
+
var PARTIAL_MATCH_DICE_THRESHOLD = 0.7;
|
|
222
266
|
async function geocode(address, options = {}) {
|
|
223
267
|
const results = await geocodeMany(address, { ...options, limit: 1 });
|
|
224
268
|
const top = results[0];
|
|
225
269
|
if (!top) return null;
|
|
226
|
-
if (top.
|
|
270
|
+
if (top.confidence_low || top.match_partial) {
|
|
271
|
+
const reason = top.confidence_low ? `score ${top.score.toFixed(2)} < seuil ${lowScoreThreshold(top.type)} (type "${top.type}")` : `libell\xE9 IGN divergent de l'adresse demand\xE9e (match_partial)`;
|
|
227
272
|
console.warn(
|
|
228
|
-
`[france-data-mcp] geocode("${address}"):
|
|
273
|
+
`[france-data-mcp] geocode("${address}"): ${reason} \u2014 r\xE9sultat incertain (label retourn\xE9: "${top.label}").`
|
|
229
274
|
);
|
|
230
275
|
}
|
|
231
276
|
return top;
|
|
@@ -239,10 +284,10 @@ async function geocodeMany(address, options = {}) {
|
|
|
239
284
|
if (type) params.set("type", type);
|
|
240
285
|
const url = `${BASE_URL2}/search/?${params.toString()}`;
|
|
241
286
|
const data = await fetchJson(url, { signal });
|
|
242
|
-
return usableGeocodeResults(data.features, `q="${address}"
|
|
287
|
+
return usableGeocodeResults(data.features, `q="${address}"`, address);
|
|
243
288
|
}
|
|
244
|
-
function usableGeocodeResults(features, context) {
|
|
245
|
-
const results = features.map(toGeocodeResult).filter((r) => r !== null);
|
|
289
|
+
function usableGeocodeResults(features, context, requestedAddress) {
|
|
290
|
+
const results = features.map((f) => toGeocodeResult(f, requestedAddress)).filter((r) => r !== null);
|
|
246
291
|
if (features.length > 0 && results.length === 0) {
|
|
247
292
|
console.warn(
|
|
248
293
|
`[france-data-mcp] geocode (${context}): IGN a renvoy\xE9 ${features.length} feature(s) mais toutes inexploitables \u2014 r\xE9sultat vide \u2260 \xAB adresse introuvable \xBB, anomalie payload IGN.`
|
|
@@ -257,10 +302,16 @@ async function reverseGeocode(point, signal) {
|
|
|
257
302
|
});
|
|
258
303
|
const url = `${BASE_URL2}/reverse/?${params.toString()}`;
|
|
259
304
|
const data = await fetchJson(url, { signal });
|
|
305
|
+
if (data.features.length === 0) {
|
|
306
|
+
console.warn(
|
|
307
|
+
`[france-data-mcp] reverseGeocode(${point.lon},${point.lat}): 0 r\xE9sultat IGN \u2014 coordonn\xE9es hors couverture (France m\xE9tropolitaine + DOM) ou en mer. Retour null.`
|
|
308
|
+
);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
260
311
|
const results = usableGeocodeResults(data.features, `reverse ${point.lon},${point.lat}`);
|
|
261
312
|
return results[0] ?? null;
|
|
262
313
|
}
|
|
263
|
-
function toGeocodeResult(feature) {
|
|
314
|
+
function toGeocodeResult(feature, requestedAddress) {
|
|
264
315
|
const coords = feature.geometry?.coordinates;
|
|
265
316
|
const point = Array.isArray(coords) ? parseCoordinates(coords[0], coords[1]) : void 0;
|
|
266
317
|
if (!point) {
|
|
@@ -276,12 +327,20 @@ function toGeocodeResult(feature) {
|
|
|
276
327
|
`[france-data-mcp] geocode: feature sans score num\xE9rique exploitable (label: "${feature.properties.label}", type: "${feature.properties.type}") \u2014 confidence_low forc\xE9 \xE0 true par prudence.`
|
|
277
328
|
);
|
|
278
329
|
}
|
|
330
|
+
const type = feature.properties.type;
|
|
331
|
+
const label = feature.properties.label;
|
|
332
|
+
const matchPartial = requestedAddress !== void 0 ? diceCoefficient(normalizeForCompare(requestedAddress), normalizeForCompare(label)) < PARTIAL_MATCH_DICE_THRESHOLD : void 0;
|
|
279
333
|
return {
|
|
280
334
|
point,
|
|
281
|
-
label
|
|
335
|
+
label,
|
|
282
336
|
score: scoreValid ? rawScore : 0,
|
|
283
|
-
confidence_low: scoreValid ? rawScore <
|
|
284
|
-
type
|
|
337
|
+
confidence_low: scoreValid ? rawScore < lowScoreThreshold(type) : true,
|
|
338
|
+
type,
|
|
339
|
+
// Spread conditionnel (pas `pickDefined`, typé string-only) : on expose
|
|
340
|
+
// `match_partial:false` quand une adresse a été demandée ET matche bien
|
|
341
|
+
// — l'absence du champ signifie "non évalué" (géocodage inverse), pas
|
|
342
|
+
// "bon match". Distinction utile au caller.
|
|
343
|
+
...matchPartial !== void 0 ? { match_partial: matchPartial } : {},
|
|
285
344
|
...pickDefined({
|
|
286
345
|
codePostal: feature.properties.postcode,
|
|
287
346
|
codeCommune: feature.properties.citycode,
|
|
@@ -302,6 +361,22 @@ function assertValidDept(dept) {
|
|
|
302
361
|
throw new RangeError(`[france-data-mcp] departement must be a valid INSEE code, got "${dept}"`);
|
|
303
362
|
}
|
|
304
363
|
|
|
364
|
+
// src/territoire/commune-index.ts
|
|
365
|
+
function parentCommuneInsee(codeInsee) {
|
|
366
|
+
if (codeInsee >= "75101" && codeInsee <= "75120") return "75056";
|
|
367
|
+
if (codeInsee >= "69381" && codeInsee <= "69389") return "69123";
|
|
368
|
+
if (codeInsee >= "13201" && codeInsee <= "13216") return "13055";
|
|
369
|
+
return codeInsee;
|
|
370
|
+
}
|
|
371
|
+
var PLM_COMMUNE_MERE_DEPT = {
|
|
372
|
+
"75056": "75",
|
|
373
|
+
"69123": "69",
|
|
374
|
+
"13055": "13"
|
|
375
|
+
};
|
|
376
|
+
function plmDept(codeInsee) {
|
|
377
|
+
return PLM_COMMUNE_MERE_DEPT[parentCommuneInsee(codeInsee)] ?? null;
|
|
378
|
+
}
|
|
379
|
+
|
|
305
380
|
// src/territoire/insee-melodi.ts
|
|
306
381
|
var MELODI_BASE_URL = "https://api.insee.fr/melodi";
|
|
307
382
|
var POPULATION_DATASET = "DS_POPULATIONS_REFERENCE";
|
|
@@ -374,6 +449,13 @@ async function getPopulationByCommune(codeInsee, options = {}) {
|
|
|
374
449
|
`Code INSEE de commune invalide: "${codeInsee}" (attendu : 5 caract\xE8res, ex "75056" ou "2A004")`
|
|
375
450
|
);
|
|
376
451
|
}
|
|
452
|
+
const communeMere = parentCommuneInsee(codeInsee);
|
|
453
|
+
if (communeMere !== codeInsee) {
|
|
454
|
+
return lookupNotFound(
|
|
455
|
+
codeInsee,
|
|
456
|
+
`Arrondissement PLM ${codeInsee} \u2014 INSEE Melodi n'expose la population qu'\xE0 la commune enti\xE8re. Utiliser le code commune-m\xE8re "${communeMere}" (population_par_commune) ou code_dept="${plmDept(codeInsee)}" (population_par_departement).`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
377
459
|
try {
|
|
378
460
|
const data = await fetchPopulation(`COM-${codeInsee}`, codeInsee, "COM", options.signal);
|
|
379
461
|
if (!data) {
|
|
@@ -415,13 +497,13 @@ async function getPopulationByDept(codeDept, options = {}) {
|
|
|
415
497
|
}
|
|
416
498
|
return lookupFound(data);
|
|
417
499
|
} catch (err) {
|
|
418
|
-
if (err instanceof HttpError && err.status === 400) {
|
|
500
|
+
if (err instanceof HttpError && (err.status === 400 || err.status === 404)) {
|
|
419
501
|
console.warn(
|
|
420
|
-
`[france-data-mcp] INSEE Melodi
|
|
502
|
+
`[france-data-mcp] INSEE Melodi ${err.status} on dept ${codeDept} \u2014 body: ${err.body ?? "<empty>"}`
|
|
421
503
|
);
|
|
422
504
|
return lookupNotFound(
|
|
423
505
|
codeDept,
|
|
424
|
-
`
|
|
506
|
+
`D\xE9partement ${codeDept} non couvert par INSEE Melodi (DS_POPULATIONS_REFERENCE) \u2014 ex: Mayotte (976) absente du dataset.`
|
|
425
507
|
);
|
|
426
508
|
}
|
|
427
509
|
console.error(
|
|
@@ -1328,18 +1410,31 @@ var SOURCE_NOTE = {
|
|
|
1328
1410
|
centroide_commune_cds: "Coordonn\xE9es CDS = centro\xEFde commune (~3 km moyenne) \u2014 pas de coords natives dans le CSV CNAM. Source : Annuaire sant\xE9 Ameli, Assurance Maladie (mention obligatoire L.1461-2 CSP). Pivot via etab_finess vers FINESS DREES pour pr\xE9cision adresse."
|
|
1329
1411
|
};
|
|
1330
1412
|
var HAVERSINE_NOTE = "Distance calcul\xE9e en vol d'oiseau (haversine PostGIS). Pour la distance routi\xE8re, croiser avec un service externe (OSRM, ORS).";
|
|
1331
|
-
|
|
1413
|
+
var CENTROIDE_COMMUNE_RESOLUTION_KM = 3;
|
|
1414
|
+
var CENTROID_PRECISIONS = /* @__PURE__ */ new Set([
|
|
1415
|
+
"centroide_commune_ameli",
|
|
1416
|
+
"centroide_commune_ans",
|
|
1417
|
+
"centroide_commune_cds"
|
|
1418
|
+
]);
|
|
1419
|
+
function isSubCommuneRadius(precision, radiusKm) {
|
|
1420
|
+
return radiusKm !== void 0 && radiusKm < CENTROIDE_COMMUNE_RESOLUTION_KM && CENTROID_PRECISIONS.has(precision);
|
|
1421
|
+
}
|
|
1422
|
+
var subCommuneRadiusNote = (radiusKm) => `radius_km=${radiusKm} < ${CENTROIDE_COMMUNE_RESOLUTION_KM} km : incompatible avec une pr\xE9cision au centro\xEFde commune. Le filtre rayon s'applique au centro\xEFde unique de chaque commune, pas aux adresses r\xE9elles \u2014 TOUS les PS d'une commune sont inclus ou exclus en bloc, et \`distance_km\` ne discrimine pas les PS d'une m\xEAme commune. Un r\xE9sultat vide peut \xEAtre un FAUX n\xE9gatif (centro\xEFde hors rayon), pas un d\xE9sert m\xE9dical. Pour une vraie g\xE9olocalisation adresse, pivoter via FINESS (etablissement_by_finess) ou \xE9largir radius_km \u2265 ${CENTROIDE_COMMUNE_RESOLUTION_KM}.`;
|
|
1423
|
+
function buildMetadata(precision, withDistance, radiusKm) {
|
|
1332
1424
|
const notes = [SOURCE_NOTE[precision]];
|
|
1333
1425
|
const result = { geo_precision: precision, notes };
|
|
1334
1426
|
if (withDistance) {
|
|
1335
1427
|
result.distance_type = "haversine_postgis";
|
|
1336
1428
|
notes.push(HAVERSINE_NOTE);
|
|
1337
1429
|
}
|
|
1430
|
+
if (isSubCommuneRadius(precision, radiusKm)) {
|
|
1431
|
+
notes.push(subCommuneRadiusNote(radiusKm));
|
|
1432
|
+
}
|
|
1338
1433
|
return result;
|
|
1339
1434
|
}
|
|
1340
1435
|
var finessRadiusMetadata = () => buildMetadata("lambert93_natif_finess", true);
|
|
1341
1436
|
var finessByCategorieMetadata = () => buildMetadata("lambert93_natif_finess", false);
|
|
1342
|
-
var rppsRadiusMetadata = () => buildMetadata("centroide_commune_ans", true);
|
|
1437
|
+
var rppsRadiusMetadata = (radiusKm) => buildMetadata("centroide_commune_ans", true, radiusKm);
|
|
1343
1438
|
var rppsDeptMetadata = () => buildMetadata("centroide_commune_ans", false);
|
|
1344
1439
|
var rppsEtablissementMetadata = () => buildMetadata("structure_finess", false);
|
|
1345
1440
|
var anonClient = null;
|
|
@@ -1451,6 +1546,17 @@ function expectRpcRows(rpc, data) {
|
|
|
1451
1546
|
}
|
|
1452
1547
|
return data;
|
|
1453
1548
|
}
|
|
1549
|
+
function buildListQueryResult(rpc, data, limit, metadata, mapRow) {
|
|
1550
|
+
const rows = expectRpcRows(rpc, data);
|
|
1551
|
+
const truncated = rows.length > limit;
|
|
1552
|
+
const sliced = truncated ? rows.slice(0, limit) : rows;
|
|
1553
|
+
return {
|
|
1554
|
+
count: sliced.length,
|
|
1555
|
+
truncated,
|
|
1556
|
+
results: sliced.map(mapRow),
|
|
1557
|
+
query_metadata: metadata
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1454
1560
|
|
|
1455
1561
|
// src/sante/finess-db.ts
|
|
1456
1562
|
function familiesToCodes(familles) {
|
|
@@ -1514,15 +1620,13 @@ async function getFinessByNumFiness(numFiness) {
|
|
|
1514
1620
|
return lookupFound(toFinessResult(first));
|
|
1515
1621
|
}
|
|
1516
1622
|
function buildFinessQueryResult(rpc, data, limit, metadata) {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
query_metadata: metadata
|
|
1525
|
-
};
|
|
1623
|
+
return buildListQueryResult(
|
|
1624
|
+
rpc,
|
|
1625
|
+
data,
|
|
1626
|
+
limit,
|
|
1627
|
+
metadata,
|
|
1628
|
+
toFinessResult
|
|
1629
|
+
);
|
|
1526
1630
|
}
|
|
1527
1631
|
function toFinessResult(row) {
|
|
1528
1632
|
const lat = row.geom?.coordinates[1];
|
|
@@ -1585,7 +1689,7 @@ async function getRppsInRadius(input) {
|
|
|
1585
1689
|
p_limit: limit + 1
|
|
1586
1690
|
});
|
|
1587
1691
|
if (error) throw new Error(formatRpcError("rpps_in_radius", error));
|
|
1588
|
-
return buildQueryResult("rpps_in_radius", data, limit, rppsRadiusMetadata());
|
|
1692
|
+
return buildQueryResult("rpps_in_radius", data, limit, rppsRadiusMetadata(input.radiusKm));
|
|
1589
1693
|
}
|
|
1590
1694
|
async function getRppsParSpecialiteDept(input) {
|
|
1591
1695
|
const limit = clampLimit(input.limit);
|
|
@@ -1614,15 +1718,13 @@ async function getRppsDansEtablissement(input) {
|
|
|
1614
1718
|
p_limit: limit + 1
|
|
1615
1719
|
});
|
|
1616
1720
|
if (error) throw new Error(formatRpcError("rpps_dans_etablissement", error));
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
query_metadata: rppsEtablissementMetadata()
|
|
1625
|
-
};
|
|
1721
|
+
return buildListQueryResult(
|
|
1722
|
+
"rpps_dans_etablissement",
|
|
1723
|
+
data,
|
|
1724
|
+
limit,
|
|
1725
|
+
rppsEtablissementMetadata(),
|
|
1726
|
+
toCompactResult
|
|
1727
|
+
);
|
|
1626
1728
|
}
|
|
1627
1729
|
async function getRppsById(rppsId) {
|
|
1628
1730
|
const trimmed = rppsId.trim();
|
|
@@ -1640,15 +1742,13 @@ async function getRppsById(rppsId) {
|
|
|
1640
1742
|
return rows.map(toLookupResult);
|
|
1641
1743
|
}
|
|
1642
1744
|
function buildQueryResult(rpc, data, limit, metadata) {
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
query_metadata: metadata
|
|
1651
|
-
};
|
|
1745
|
+
return buildListQueryResult(
|
|
1746
|
+
rpc,
|
|
1747
|
+
data,
|
|
1748
|
+
limit,
|
|
1749
|
+
metadata,
|
|
1750
|
+
toResult
|
|
1751
|
+
);
|
|
1652
1752
|
}
|
|
1653
1753
|
function toResult(row) {
|
|
1654
1754
|
const lat = row.geom?.coordinates[1];
|