From e13369d4e3ba60722c1f240c1e4c9f23c2b62a59 Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Wed, 17 Dec 2025 16:13:15 +0100 Subject: [PATCH 1/5] chore: sanitize dpe before running --- README.md | 2 +- dist/reports/corpus/corpus_list_main.json | 2 +- src/dpe-sanitizer.service.js | 90 +++++++++++++++++++++++ src/engine.js | 66 ++++++++++++++++- src/utils.js | 32 ++++++++ test/test-helpers.js | 37 +--------- 6 files changed, 189 insertions(+), 40 deletions(-) create mode 100644 src/dpe-sanitizer.service.js diff --git a/README.md b/README.md index 5078eab..c3c5e4d 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ Nous accueillons les contributions avec plaisir ! Si vous souhaitez améliorer O ## Licence -Distribué sous la license `GPL-3.0 license`. Lire le fichier `LICENSE` pour plus d'informations. +Distribué sous la license `MIT`. Lire le fichier `LICENSE` pour plus d'informations.

(Retour sommaire)

diff --git a/dist/reports/corpus/corpus_list_main.json b/dist/reports/corpus/corpus_list_main.json index d0df516..3b249cc 100644 --- a/dist/reports/corpus/corpus_list_main.json +++ b/dist/reports/corpus/corpus_list_main.json @@ -9,5 +9,5 @@ "dpe_appartement_individuel_chauffage_individuel_2025.csv", "dpe_appartement_individuel_chauffage_collectif_2025.csv" ], - "branches": ["main", "fix_issue_132"] + "branches": ["main"] } diff --git a/src/dpe-sanitizer.service.js b/src/dpe-sanitizer.service.js new file mode 100644 index 0000000..ed16983 --- /dev/null +++ b/src/dpe-sanitizer.service.js @@ -0,0 +1,90 @@ +import { ObjectUtil } from './core/util/infrastructure/object-util.js'; + +const nodesToMap = [ + 'mur', + 'plancher_bas', + 'plancher_haut', + 'baie_vitree', + 'porte', + 'pont_thermique', + 'ventilation', + 'installation_ecs', + 'generateur_ecs', + 'climatisation', + 'installation_chauffage', + 'generateur_chauffage', + 'emetteur_chauffage', + 'sortie_par_energie' +]; + +/** + * Transform single nodes in {@link nodesToMap} into array of nodes. + * Transform string number into digits + * These transformations should be done inside the open3cl library + * + * @example + * // Will transform + * "plancher_haut_collection": { + * "plancher_haut": {"id": 1} + * } + * // Into + * "plancher_haut_collection": { + * "plancher_haut": [{"id": 1}] + * } + * + * @example + * // Will transform + * "surface_paroi_opaque": "40.94" + * // Into + * "surface_paroi_opaque": 40.94 + */ +export default class DpeSanitizerService { + /** + * @param dpe {FullDpe} + * @return {FullDpe} + */ + execute(dpe) { + return ObjectUtil.deepObjectTransform( + dpe, + (key) => key, + (val, key) => { + if (this.#needTransform(key, val)) { + return [val]; + } + + if (this.#isEnum(key)) { + return val; + } + + if (this.#isUndefinedVal(val)) { + return ''; + } + + if (this.#isEmptyArray(val)) { + return val; + } + + if (Number.isNaN(Number(val))) { + return val; + } + return Number(val); + } + ); + } + + #isEnum(key) { + return key.startsWith('enum_') || key.startsWith('original_enum'); + } + + #needTransform(key, val) { + return typeof val === 'object' && !Array.isArray(val) && nodesToMap.includes(key); + } + + #isUndefinedVal(val) { + return val === '' || val === null; + } + + #isEmptyArray(val) { + return Array.isArray(val) && val.length === 0; + } +} diff --git a/src/engine.js b/src/engine.js index e5112ee..91006c5 100644 --- a/src/engine.js +++ b/src/engine.js @@ -19,14 +19,20 @@ import { collectionCanBeEmpty, containsAnySubstring, isEffetJoule, - sanitize_dpe + sanitize_dpe, + xmlParser } from './utils.js'; import { Inertie } from './7_inertie.js'; import getFicheTechnique from './ficheTechnique.js'; import { ProductionENR } from './16.2_production_enr.js'; +import { XMLParser } from 'fast-xml-parser'; +import DpeSanitizerService from './dpe-sanitizer.service.js'; +import fs from 'node:fs'; const LIB_VERSION = 'OPEN3CL_VERSION'; +const dpeSanitizerService = new DpeSanitizerService(); + function calc_th(map_id) { const map = enums.methode_application_dpe_log[map_id]; if (map.includes('maison')) return 'maison'; @@ -44,11 +50,67 @@ export function getVersion() { } /** + * @param dpe {FullDpe} A plain object with exhaustive dpe info + * @return {FullDpe} + */ +export function run(dpe) { + return calcul_3cl(dpe); +} + +/** + * @param xmlFilePath {string} A path to the dpe xml file to analyze + * @return {FullDpe} + */ +export function runFromXmlFile(xmlFilePath) { + /** @type {string} **/ + const dpeXmlContent = fs.readFileSync(xmlFilePath, { encoding: 'utf8', flag: 'r' }); + return runFromXmlString(dpeXmlContent); +} + +/** + * @param dpeXmlContent {string} A full dpe xml content + * @return {FullDpe} + */ +export function runFromXmlString(dpeXmlContent) { + /** @type {{dpe: FullDpe}} **/ + const xmlDpe = xmlParser.parse(dpeXmlContent); + return run(xmlDpe.dpe); +} + +/** + * Will download the dpe by its code via ADEME apis, and it will run the engine. + * Environments variables `ADEME_CLIENT_ID` and `ADEME_CLIENT_SECRET` must be set. + * + * @param dpeCode {string} The dpe code to retrieve from ADEME and to analyze + * @return {Promise} + */ +export async function runFromAdeme(dpeCode) { + if (!process.env.ADEME_CLIENT_ID || !process.env.ADEME_CLIENT_SECRET) { + throw new Error(`ADEME_CLIENT_ID and ADEME_CLIENT_SECRET environment variables are not set !`); + } + + const response = await fetch( + `https://prd-x-ademe-externe-api.de-c1.eu1.cloudhub.io/api/v1/pub/dpe/${dpeCode}/xml`, + { + headers: { + client_id: process.env.ADEME_CLIENT_ID, + client_secret: process.env.ADEME_CLIENT_SECRET + } + } + ); + if (response.status !== 200) { + throw new Error(`Fail to retrieve DPE from ADEME: ${dpeCode}`); + } + return runFromXmlString(await response.text()); +} + +/** + * @deprecated * @param dpe {FullDpe} * @return {FullDpe} */ export function calcul_3cl(dpe) { - sanitize_dpe(dpe); + dpeSanitizerService.execute(dpe); const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id]; const dateDpe = dpe.administratif.date_etablissement_dpe; if (modele !== 'dpe 3cl 2021 méthode logement') { diff --git a/src/utils.js b/src/utils.js index 68247d3..3718afc 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,38 @@ import enums from './enums.js'; import tvs from './tv.js'; import { set, has } from 'lodash-es'; +import { XMLParser } from 'fast-xml-parser'; + +export const xmlParser = new XMLParser({ + // We want to make sure collections of length 1 are still parsed as arrays + isArray: (name) => { + const collectionNames = [ + 'mur', + 'plancher_bas', + 'plancher_haut', + 'baie_vitree', + 'porte', + 'pont_thermique', + 'ventilation', + 'installation_ecs', + 'generateur_ecs', + 'climatisation', + 'installation_chauffage', + 'generateur_chauffage', + 'emetteur_chauffage', + 'sortie_par_energie' + ]; + if (collectionNames.includes(name)) return true; + }, + tagValueProcessor: (tagName, val) => { + if (tagName.startsWith('enum_')) { + // Preserve value as string for tags starting with "enum_" + return null; + } + if (Number.isNaN(Number(val))) return val; + return Number(val); + } +}); export let bug_for_bug_compat = false; export function set_bug_for_bug_compat() { diff --git a/test/test-helpers.js b/test/test-helpers.js index beca3ea..7b7a58c 100644 --- a/test/test-helpers.js +++ b/test/test-helpers.js @@ -1,40 +1,5 @@ -import { XMLParser } from 'fast-xml-parser'; import fs from 'node:fs'; - -const xmlParser = new XMLParser({ - // We want to make sure collections of length 1 are still parsed as arrays - isArray: (name) => { - const collectionNames = [ - 'mur', - 'plancher_bas', - 'plancher_haut', - 'baie_vitree', - 'porte', - 'pont_thermique', - 'ventilation', - 'installation_ecs', - 'generateur_ecs', - 'climatisation', - 'installation_chauffage', - 'generateur_chauffage', - 'emetteur_chauffage', - 'sortie_par_energie' - ]; - if (collectionNames.includes(name)) return true; - }, - tagValueProcessor: (tagName, val) => { - if (tagName.startsWith('enum_')) { - // Preserve value as string for tags starting with "enum_" - return null; - } - if (Number.isNaN(Number(val))) return val; - return Number(val); - } -}); - -export function parseXml(data) { - return xmlParser.parse(data).dpe; -} +import { xmlParser } from '../src/utils.js'; export async function getAdemeFileJsonOrDownload(dpeCode) { if (!process.env.ADEME_CLIENT_ID || !process.env.ADEME_CLIENT_SECRET) { From 6aa67606f260e22e27bc5da585733c0533917f6d Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Wed, 17 Dec 2025 16:42:16 +0100 Subject: [PATCH 2/5] chore: sanitize dpe before running --- src/engine.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine.js b/src/engine.js index 91006c5..07874b8 100644 --- a/src/engine.js +++ b/src/engine.js @@ -110,7 +110,7 @@ export async function runFromAdeme(dpeCode) { * @return {FullDpe} */ export function calcul_3cl(dpe) { - dpeSanitizerService.execute(dpe); + sanitize_dpe(dpe); const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id]; const dateDpe = dpe.administratif.date_etablissement_dpe; if (modele !== 'dpe 3cl 2021 méthode logement') { From 12a77c98911836bba31c23d29b850d267c4f1295 Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Mon, 9 Feb 2026 22:15:04 +0100 Subject: [PATCH 3/5] fix: add dpe sanitization --- index.js | 3 ++- src/engine.js | 63 ++++++++------------------------------------------- src/index.js | 10 ++++++-- src/utils.js | 34 +-------------------------- 4 files changed, 20 insertions(+), 90 deletions(-) diff --git a/index.js b/index.js index 439f4d6..3190677 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,10 @@ import { calcul_3cl, + calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion } from './src/index.js'; -export { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; +export { calcul_3cl, calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; import { Umur, Uph, Upb, Uporte, Ubv, Upt, calc_deperdition } from './src/3_deperdition.js'; export { Umur, Uph, Upb, Uporte, Ubv, Upt, calc_deperdition }; diff --git a/src/engine.js b/src/engine.js index 07874b8..8253d87 100644 --- a/src/engine.js +++ b/src/engine.js @@ -19,15 +19,12 @@ import { collectionCanBeEmpty, containsAnySubstring, isEffetJoule, - sanitize_dpe, xmlParser } from './utils.js'; import { Inertie } from './7_inertie.js'; import getFicheTechnique from './ficheTechnique.js'; import { ProductionENR } from './16.2_production_enr.js'; -import { XMLParser } from 'fast-xml-parser'; import DpeSanitizerService from './dpe-sanitizer.service.js'; -import fs from 'node:fs'; const LIB_VERSION = 'OPEN3CL_VERSION'; @@ -50,67 +47,25 @@ export function getVersion() { } /** - * @param dpe {FullDpe} A plain object with exhaustive dpe info - * @return {FullDpe} - */ -export function run(dpe) { - return calcul_3cl(dpe); -} - -/** - * @param xmlFilePath {string} A path to the dpe xml file to analyze - * @return {FullDpe} - */ -export function runFromXmlFile(xmlFilePath) { - /** @type {string} **/ - const dpeXmlContent = fs.readFileSync(xmlFilePath, { encoding: 'utf8', flag: 'r' }); - return runFromXmlString(dpeXmlContent); -} - -/** + * Run the engine with a full dpe xml content * @param dpeXmlContent {string} A full dpe xml content + * @param options {{sanitize: boolean}?} * @return {FullDpe} */ -export function runFromXmlString(dpeXmlContent) { +export function calcul_3cl_xml(dpeXmlContent, options) { /** @type {{dpe: FullDpe}} **/ const xmlDpe = xmlParser.parse(dpeXmlContent); - return run(xmlDpe.dpe); -} - -/** - * Will download the dpe by its code via ADEME apis, and it will run the engine. - * Environments variables `ADEME_CLIENT_ID` and `ADEME_CLIENT_SECRET` must be set. - * - * @param dpeCode {string} The dpe code to retrieve from ADEME and to analyze - * @return {Promise} - */ -export async function runFromAdeme(dpeCode) { - if (!process.env.ADEME_CLIENT_ID || !process.env.ADEME_CLIENT_SECRET) { - throw new Error(`ADEME_CLIENT_ID and ADEME_CLIENT_SECRET environment variables are not set !`); - } - - const response = await fetch( - `https://prd-x-ademe-externe-api.de-c1.eu1.cloudhub.io/api/v1/pub/dpe/${dpeCode}/xml`, - { - headers: { - client_id: process.env.ADEME_CLIENT_ID, - client_secret: process.env.ADEME_CLIENT_SECRET - } - } - ); - if (response.status !== 200) { - throw new Error(`Fail to retrieve DPE from ADEME: ${dpeCode}`); - } - return runFromXmlString(await response.text()); + return calcul_3cl(xmlDpe.dpe, options); } /** - * @deprecated - * @param dpe {FullDpe} + * Run the engine with a javascript plain object dpe + * @param inputDpe {FullDpe} A full plain object dpe + * @param options {{sanitize: boolean}?} * @return {FullDpe} */ -export function calcul_3cl(dpe) { - sanitize_dpe(dpe); +export function calcul_3cl(inputDpe, options) { + const dpe = options?.sanitize ? dpeSanitizerService.execute(inputDpe) : inputDpe; const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id]; const dateDpe = dpe.administratif.date_etablissement_dpe; if (modele !== 'dpe 3cl 2021 méthode logement') { diff --git a/src/index.js b/src/index.js index 5a11ecc..77cc80c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,10 @@ -import { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion } from './engine.js'; -export { calcul_3cl, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; +import { + calcul_3cl, + calcul_3cl_xml, + get_classe_ges_dpe, + get_conso_coeff_1_9_2026, + getVersion +} from './engine.js'; +export { calcul_3cl, calcul_3cl_xml, get_classe_ges_dpe, get_conso_coeff_1_9_2026, getVersion }; import { Umur, Uph, Upb, Uporte, Ubv, Upt } from './3_deperdition.js'; export { Umur, Uph, Upb, Uporte, Ubv, Upt }; diff --git a/src/utils.js b/src/utils.js index 3718afc..4ecfd87 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ import enums from './enums.js'; import tvs from './tv.js'; -import { set, has } from 'lodash-es'; +import { set } from 'lodash-es'; import { XMLParser } from 'fast-xml-parser'; export const xmlParser = new XMLParser({ @@ -317,44 +317,12 @@ export function removeKeyFromJSON(jsonObj, keyToRemove, skipKeys) { } } -export function useEnumAsString(jsonObj) { - for (const key in jsonObj) { - if (jsonObj.hasOwnProperty(key)) { - if (key.startsWith('enum_')) { - if (jsonObj[key] !== null) jsonObj[key] = jsonObj[key].toString(); - } else if (typeof jsonObj[key] === 'object') { - useEnumAsString(jsonObj[key]); - } - } - } -} - export function clean_dpe(dpe_in) { // skip generateur_[ecs|chauffage] because some input data is contained in donnee_intermediaire (e.g. pn, qp0, ...) removeKeyFromJSON(dpe_in, 'donnee_intermediaire', ['generateur_ecs', 'generateur_chauffage']); set(dpe_in, 'logement.sortie', null); } -export function sanitize_dpe(dpe_in) { - const collection_paths = [ - 'logement.enveloppe.plancher_bas_collection.plancher_bas', - 'logement.enveloppe.plancher_haut_collection.plancher_haut', - 'logement.ventilation_collection.ventilation', - 'logement.climatisation_collection.climatisation', - 'logement.enveloppe.baie_vitree_collection.baie_vitree', - 'logement.enveloppe.porte_collection.porte', - 'logement.enveloppe.pont_thermique_collection.pont_thermique' - ]; - for (const path of collection_paths) { - if (!has(dpe_in, path)) { - set(dpe_in, path, []); - } - } - if (use_enum_as_string) { - useEnumAsString(dpe_in); - } -} - /** * Retrieve a number describing a thickness from the description * @param description string in which to get the number From f7ae16dc30c19b8336ccb6e7a0c65ace1cd228cd Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Mon, 9 Feb 2026 22:33:56 +0100 Subject: [PATCH 4/5] docs: update readme file --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3c5e4d..e9b7d25 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ C'est un bon moyen de détecter un éventuel problème dans le dpe ou la librair ```javascript import { calcul_3cl } from 'open3cl'; -// Exemple d'objet JSON issu d'un fichier XML DPE +// Exemple d'objet JSON (partiel) issu d'un fichier XML DPE const dpeData = { numero_dpe: '2113E1018248X', statut: 'ACTIF', @@ -108,7 +108,23 @@ const dpeData = { } }; +// Execution d'un dpe avec la librairie Open3CL sans aucune transformation / nettoyage du dpe const result = calcul_3cl(dpeData); +const result = calcul_3cl(dpeData, { sanitize: false }); + +// Execution d'un dpe avec la librairie Open3CL avec pré-transformation / nettoyage du dpe +const result = calcul_3cl(dpeData, { sanitize: true }); + +// Execution d'un dpe au format xml avec la librairie Open3CL sans aucune transformation / nettoyage du dpe +const result = calcul_3cl_xml('2113E1018248X'); +const result = calcul_3cl_xml('2113E1018248X', { + sanitize: false +}); + +// Execution d'un dpe au format xml avec la librairie Open3CL avec pré-transformation / nettoyage du dpe +const result = calcul_3cl_xml('2113E1018248X', { + sanitize: true +}); ``` ## Variables d'environnements From c40c50b55f6fd317f78da554101ba7ee5e7d5c2c Mon Sep 17 00:00:00 2001 From: Michael Desigaud Date: Mon, 9 Feb 2026 22:38:01 +0100 Subject: [PATCH 5/5] docs: update readme file --- README.md | 16 ++++++++-------- src/engine.js | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e9b7d25..72d1970 100644 --- a/README.md +++ b/README.md @@ -108,22 +108,22 @@ const dpeData = { } }; -// Execution d'un dpe avec la librairie Open3CL sans aucune transformation / nettoyage du dpe +// Execution d'un dpe avec la librairie Open3CL avec pré-transformation / nettoyage du dpe (comportement par défaut) const result = calcul_3cl(dpeData); -const result = calcul_3cl(dpeData, { sanitize: false }); - -// Execution d'un dpe avec la librairie Open3CL avec pré-transformation / nettoyage du dpe const result = calcul_3cl(dpeData, { sanitize: true }); -// Execution d'un dpe au format xml avec la librairie Open3CL sans aucune transformation / nettoyage du dpe +// Execution d'un dpe avec la librairie Open3CL sans pré-transformation / nettoyage du dpe +const result = calcul_3cl(dpeData, { sanitize: false }); + +// Execution d'un dpe au format xml avec la librairie Open3CL avec pré-transformation / nettoyage du dpe (comportement par défaut) const result = calcul_3cl_xml('2113E1018248X'); const result = calcul_3cl_xml('2113E1018248X', { - sanitize: false + sanitize: true }); -// Execution d'un dpe au format xml avec la librairie Open3CL avec pré-transformation / nettoyage du dpe +// Execution d'un dpe au format xml avec la librairie Open3CL sans pré-transformation / nettoyage du dpe (comportement par défaut) const result = calcul_3cl_xml('2113E1018248X', { - sanitize: true + sanitize: false }); ``` diff --git a/src/engine.js b/src/engine.js index 8253d87..d748589 100644 --- a/src/engine.js +++ b/src/engine.js @@ -65,7 +65,8 @@ export function calcul_3cl_xml(dpeXmlContent, options) { * @return {FullDpe} */ export function calcul_3cl(inputDpe, options) { - const dpe = options?.sanitize ? dpeSanitizerService.execute(inputDpe) : inputDpe; + if (!options) options = { sanitize: true }; + const dpe = options.sanitize ? dpeSanitizerService.execute(inputDpe) : inputDpe; const modele = enums.modele_dpe[dpe.administratif.enum_modele_dpe_id]; const dateDpe = dpe.administratif.date_etablissement_dpe; if (modele !== 'dpe 3cl 2021 méthode logement') {