@communecter/cocolight-api-client 1.0.155 → 1.0.157
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/1035.cocolight-api-client.browser.js +1 -1
- package/dist/1035.cocolight-api-client.cjs +1 -1
- package/dist/1483.cocolight-api-client.browser.js +1 -1
- package/dist/1483.cocolight-api-client.cjs +1 -1
- package/dist/1491.cocolight-api-client.browser.js +1 -1
- package/dist/1491.cocolight-api-client.cjs +1 -1
- package/dist/1494.cocolight-api-client.browser.js +1 -1
- package/dist/1494.cocolight-api-client.cjs +1 -1
- package/dist/1641.cocolight-api-client.browser.js +1 -1
- package/dist/1641.cocolight-api-client.cjs +1 -1
- package/dist/1678.cocolight-api-client.browser.js +1 -1
- package/dist/1678.cocolight-api-client.cjs +1 -1
- package/dist/2041.cocolight-api-client.browser.js +1 -1
- package/dist/2041.cocolight-api-client.cjs +1 -1
- package/dist/2302.cocolight-api-client.browser.js +1 -1
- package/dist/2302.cocolight-api-client.cjs +1 -1
- package/dist/2313.cocolight-api-client.browser.js +1 -1
- package/dist/2313.cocolight-api-client.cjs +1 -1
- package/dist/2379.cocolight-api-client.browser.js +1 -1
- package/dist/2379.cocolight-api-client.cjs +1 -1
- package/dist/2439.cocolight-api-client.browser.js +1 -1
- package/dist/2439.cocolight-api-client.cjs +1 -1
- package/dist/2594.cocolight-api-client.browser.js +1 -1
- package/dist/2594.cocolight-api-client.cjs +1 -1
- package/dist/2766.cocolight-api-client.browser.js +1 -1
- package/dist/2766.cocolight-api-client.cjs +1 -1
- package/dist/2812.cocolight-api-client.browser.js +1 -1
- package/dist/2812.cocolight-api-client.cjs +1 -1
- package/dist/2918.cocolight-api-client.browser.js +1 -1
- package/dist/2918.cocolight-api-client.cjs +1 -1
- package/dist/3028.cocolight-api-client.browser.js +1 -1
- package/dist/3028.cocolight-api-client.cjs +1 -1
- package/dist/3330.cocolight-api-client.browser.js +1 -1
- package/dist/3330.cocolight-api-client.cjs +1 -1
- package/dist/3371.cocolight-api-client.browser.js +1 -1
- package/dist/3371.cocolight-api-client.cjs +1 -1
- package/dist/3401.cocolight-api-client.browser.js +1 -1
- package/dist/3401.cocolight-api-client.cjs +1 -1
- package/dist/3516.cocolight-api-client.browser.js +1 -1
- package/dist/3516.cocolight-api-client.cjs +1 -1
- package/dist/3593.cocolight-api-client.browser.js +1 -1
- package/dist/3593.cocolight-api-client.cjs +1 -1
- package/dist/3764.cocolight-api-client.browser.js +1 -1
- package/dist/3764.cocolight-api-client.cjs +1 -1
- package/dist/3873.cocolight-api-client.browser.js +1 -1
- package/dist/3873.cocolight-api-client.cjs +1 -1
- package/dist/3911.cocolight-api-client.browser.js +1 -1
- package/dist/3911.cocolight-api-client.cjs +1 -1
- package/dist/4031.cocolight-api-client.browser.js +1 -1
- package/dist/4031.cocolight-api-client.cjs +1 -1
- package/dist/4152.cocolight-api-client.browser.js +1 -1
- package/dist/4152.cocolight-api-client.cjs +1 -1
- package/dist/4203.cocolight-api-client.browser.js +1 -1
- package/dist/4203.cocolight-api-client.cjs +1 -1
- package/dist/424.cocolight-api-client.browser.js +1 -1
- package/dist/424.cocolight-api-client.cjs +1 -1
- package/dist/435.cocolight-api-client.browser.js +1 -1
- package/dist/435.cocolight-api-client.cjs +1 -1
- package/dist/4402.cocolight-api-client.browser.js +1 -1
- package/dist/4402.cocolight-api-client.cjs +1 -1
- package/dist/4425.cocolight-api-client.browser.js +1 -1
- package/dist/4425.cocolight-api-client.cjs +1 -1
- package/dist/4472.cocolight-api-client.browser.js +1 -1
- package/dist/4472.cocolight-api-client.cjs +1 -1
- package/dist/4914.cocolight-api-client.browser.js +1 -1
- package/dist/4914.cocolight-api-client.cjs +1 -1
- package/dist/4959.cocolight-api-client.browser.js +1 -1
- package/dist/4959.cocolight-api-client.cjs +1 -1
- package/dist/5100.cocolight-api-client.browser.js +1 -1
- package/dist/5100.cocolight-api-client.cjs +1 -1
- package/dist/5111.cocolight-api-client.browser.js +1 -1
- package/dist/5111.cocolight-api-client.cjs +1 -1
- package/dist/5286.cocolight-api-client.browser.js +1 -1
- package/dist/5286.cocolight-api-client.cjs +1 -1
- package/dist/5380.cocolight-api-client.browser.js +1 -1
- package/dist/5380.cocolight-api-client.cjs +1 -1
- package/dist/5383.cocolight-api-client.browser.js +1 -1
- package/dist/5383.cocolight-api-client.cjs +1 -1
- package/dist/5480.cocolight-api-client.browser.js +1 -1
- package/dist/5480.cocolight-api-client.cjs +1 -1
- package/dist/5541.cocolight-api-client.browser.js +1 -1
- package/dist/5541.cocolight-api-client.cjs +1 -1
- package/dist/5839.cocolight-api-client.browser.js +1 -1
- package/dist/5839.cocolight-api-client.cjs +1 -1
- package/dist/5862.cocolight-api-client.browser.js +1 -1
- package/dist/5862.cocolight-api-client.cjs +1 -1
- package/dist/5869.cocolight-api-client.browser.js +1 -1
- package/dist/5869.cocolight-api-client.cjs +1 -1
- package/dist/588.cocolight-api-client.browser.js +1 -1
- package/dist/588.cocolight-api-client.cjs +1 -1
- package/dist/601.cocolight-api-client.browser.js +1 -1
- package/dist/601.cocolight-api-client.cjs +1 -1
- package/dist/6012.cocolight-api-client.browser.js +1 -1
- package/dist/6012.cocolight-api-client.cjs +1 -1
- package/dist/6200.cocolight-api-client.browser.js +1 -1
- package/dist/6200.cocolight-api-client.cjs +1 -1
- package/dist/6369.cocolight-api-client.browser.js +1 -1
- package/dist/6369.cocolight-api-client.cjs +1 -1
- package/dist/7009.cocolight-api-client.browser.js +1 -1
- package/dist/7009.cocolight-api-client.cjs +1 -1
- package/dist/725.cocolight-api-client.browser.js +1 -1
- package/dist/725.cocolight-api-client.cjs +1 -1
- package/dist/7320.cocolight-api-client.browser.js +1 -1
- package/dist/7339.cocolight-api-client.browser.js +1 -1
- package/dist/7339.cocolight-api-client.cjs +1 -1
- package/dist/7398.cocolight-api-client.browser.js +1 -1
- package/dist/7398.cocolight-api-client.cjs +1 -1
- package/dist/7888.cocolight-api-client.browser.js +1 -1
- package/dist/7888.cocolight-api-client.cjs +1 -1
- package/dist/7975.cocolight-api-client.browser.js +1 -1
- package/dist/7975.cocolight-api-client.cjs +1 -1
- package/dist/809.cocolight-api-client.browser.js +1 -1
- package/dist/809.cocolight-api-client.cjs +1 -1
- package/dist/8300.cocolight-api-client.browser.js +1 -1
- package/dist/8300.cocolight-api-client.cjs +1 -1
- package/dist/8317.cocolight-api-client.browser.js +1 -1
- package/dist/8317.cocolight-api-client.cjs +1 -1
- package/dist/8525.cocolight-api-client.browser.js +1 -1
- package/dist/8525.cocolight-api-client.cjs +1 -1
- package/dist/8894.cocolight-api-client.browser.js +1 -1
- package/dist/8894.cocolight-api-client.cjs +1 -1
- package/dist/9301.cocolight-api-client.browser.js +1 -1
- package/dist/9301.cocolight-api-client.cjs +1 -1
- package/dist/9438.cocolight-api-client.browser.js +1 -1
- package/dist/9438.cocolight-api-client.cjs +1 -1
- package/dist/9478.cocolight-api-client.browser.js +1 -1
- package/dist/9478.cocolight-api-client.cjs +1 -1
- package/dist/9568.cocolight-api-client.browser.js +1 -1
- package/dist/9568.cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.browser.js +6 -6
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.vite.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js.map +1 -1
- package/package.json +7 -17
- package/types/Api.d.ts +1 -0
- package/types/ApiClient.d.ts +10 -1
- package/types/api/Answer.d.ts +27 -9
- package/types/api/BaseEntity.d.ts +77 -0
- package/types/api/EndpointApi.d.ts +51 -3
- package/types/api/EndpointApi.types.d.ts +112 -1
- package/types/api/User.d.ts +20 -0
- package/types/costum/types.d.ts +0 -2
- package/types/endpoints.module.d.ts +3039 -1009
- package/types/index.d.ts +1 -0
- package/types/utils/OfflineClientManager.d.ts +8 -0
- package/types/utils/adminImportExport.d.ts +29 -0
- package/dist/1035.cocolight-api-client.mjs.js +0 -1
- package/dist/1483.cocolight-api-client.mjs.js +0 -1
- package/dist/1491.cocolight-api-client.mjs.js +0 -1
- package/dist/1494.cocolight-api-client.mjs.js +0 -1
- package/dist/1641.cocolight-api-client.mjs.js +0 -1
- package/dist/1678.cocolight-api-client.mjs.js +0 -1
- package/dist/2041.cocolight-api-client.mjs.js +0 -1
- package/dist/2302.cocolight-api-client.mjs.js +0 -1
- package/dist/2313.cocolight-api-client.mjs.js +0 -1
- package/dist/2379.cocolight-api-client.mjs.js +0 -1
- package/dist/2439.cocolight-api-client.mjs.js +0 -1
- package/dist/2594.cocolight-api-client.mjs.js +0 -1
- package/dist/2766.cocolight-api-client.mjs.js +0 -1
- package/dist/2812.cocolight-api-client.mjs.js +0 -1
- package/dist/2918.cocolight-api-client.mjs.js +0 -1
- package/dist/3028.cocolight-api-client.mjs.js +0 -1
- package/dist/3330.cocolight-api-client.mjs.js +0 -1
- package/dist/3371.cocolight-api-client.mjs.js +0 -1
- package/dist/3401.cocolight-api-client.mjs.js +0 -1
- package/dist/3516.cocolight-api-client.mjs.js +0 -1
- package/dist/3593.cocolight-api-client.mjs.js +0 -1
- package/dist/3764.cocolight-api-client.mjs.js +0 -1
- package/dist/3873.cocolight-api-client.mjs.js +0 -1
- package/dist/3911.cocolight-api-client.mjs.js +0 -1
- package/dist/4031.cocolight-api-client.mjs.js +0 -1
- package/dist/4152.cocolight-api-client.mjs.js +0 -1
- package/dist/4203.cocolight-api-client.mjs.js +0 -1
- package/dist/424.cocolight-api-client.mjs.js +0 -1
- package/dist/435.cocolight-api-client.mjs.js +0 -1
- package/dist/4402.cocolight-api-client.mjs.js +0 -1
- package/dist/4425.cocolight-api-client.mjs.js +0 -1
- package/dist/4472.cocolight-api-client.mjs.js +0 -1
- package/dist/4914.cocolight-api-client.mjs.js +0 -1
- package/dist/4959.cocolight-api-client.mjs.js +0 -1
- package/dist/5100.cocolight-api-client.mjs.js +0 -1
- package/dist/5111.cocolight-api-client.mjs.js +0 -1
- package/dist/5286.cocolight-api-client.mjs.js +0 -1
- package/dist/5380.cocolight-api-client.mjs.js +0 -1
- package/dist/5383.cocolight-api-client.mjs.js +0 -1
- package/dist/5480.cocolight-api-client.mjs.js +0 -1
- package/dist/5541.cocolight-api-client.mjs.js +0 -1
- package/dist/5839.cocolight-api-client.mjs.js +0 -1
- package/dist/5862.cocolight-api-client.mjs.js +0 -1
- package/dist/5869.cocolight-api-client.mjs.js +0 -1
- package/dist/588.cocolight-api-client.mjs.js +0 -1
- package/dist/601.cocolight-api-client.mjs.js +0 -1
- package/dist/6012.cocolight-api-client.mjs.js +0 -1
- package/dist/6200.cocolight-api-client.mjs.js +0 -1
- package/dist/6369.cocolight-api-client.mjs.js +0 -1
- package/dist/7009.cocolight-api-client.mjs.js +0 -1
- package/dist/725.cocolight-api-client.mjs.js +0 -1
- package/dist/7339.cocolight-api-client.mjs.js +0 -1
- package/dist/7398.cocolight-api-client.mjs.js +0 -1
- package/dist/7888.cocolight-api-client.mjs.js +0 -1
- package/dist/7975.cocolight-api-client.mjs.js +0 -1
- package/dist/809.cocolight-api-client.mjs.js +0 -1
- package/dist/8300.cocolight-api-client.mjs.js +0 -1
- package/dist/8317.cocolight-api-client.mjs.js +0 -1
- package/dist/8525.cocolight-api-client.mjs.js +0 -1
- package/dist/8894.cocolight-api-client.mjs.js +0 -1
- package/dist/9301.cocolight-api-client.mjs.js +0 -1
- package/dist/9438.cocolight-api-client.mjs.js +0 -1
- package/dist/9478.cocolight-api-client.mjs.js +0 -1
- package/dist/9568.cocolight-api-client.mjs.js +0 -1
- package/dist/cocolight-api-client.mjs.js +0 -1
- package/src/Api.ts +0 -348
- package/src/ApiClient.ts +0 -1994
- package/src/EJSONType.ts +0 -103
- package/src/api/Action.ts +0 -665
- package/src/api/Answer.ts +0 -1392
- package/src/api/Badge.ts +0 -143
- package/src/api/BaseEntity.ts +0 -6384
- package/src/api/Classified.ts +0 -175
- package/src/api/Comment.ts +0 -280
- package/src/api/EndpointApi.ts +0 -2270
- package/src/api/EndpointApi.types.ts +0 -6566
- package/src/api/EntityRegistry.ts +0 -324
- package/src/api/Event.ts +0 -360
- package/src/api/Form.ts +0 -282
- package/src/api/News.ts +0 -547
- package/src/api/Notifications.ts +0 -196
- package/src/api/Organization.ts +0 -771
- package/src/api/Poi.ts +0 -242
- package/src/api/Project.ts +0 -545
- package/src/api/User.ts +0 -1889
- package/src/api/UserApi.ts +0 -192
- package/src/api/serverDataType/Action.ts +0 -177
- package/src/api/serverDataType/Answer.ts +0 -106
- package/src/api/serverDataType/Classified.ts +0 -152
- package/src/api/serverDataType/Comment.ts +0 -88
- package/src/api/serverDataType/Country.ts +0 -1
- package/src/api/serverDataType/Event.ts +0 -112
- package/src/api/serverDataType/Form.ts +0 -157
- package/src/api/serverDataType/News.ts +0 -138
- package/src/api/serverDataType/Notification.ts +0 -81
- package/src/api/serverDataType/Organization.ts +0 -111
- package/src/api/serverDataType/Poi.ts +0 -75
- package/src/api/serverDataType/Project.ts +0 -115
- package/src/api/serverDataType/User.ts +0 -112
- package/src/api/serverDataType/Zone.ts +0 -33
- package/src/api/serverDataType/common.ts +0 -84
- package/src/costum/normalize.ts +0 -7
- package/src/costum/registry.ts +0 -138
- package/src/costum/runtime-map.ts +0 -221
- package/src/costum/runtime.ts +0 -274
- package/src/costum/schemas/CercleTravailleursSociauxLiberauxLaPlumeSociale.ts +0 -19
- package/src/costum/schemas/FabEn.ts +0 -19
- package/src/costum/schemas/LivingLabInnovationSocialeBraspanon.ts +0 -19
- package/src/costum/schemas/alfph.ts +0 -27
- package/src/costum/schemas/campagne.ts +0 -19
- package/src/costum/schemas/chtitube.ts +0 -19
- package/src/costum/schemas/circuitCourt.ts +0 -19
- package/src/costum/schemas/cmcasHauteBretagne.ts +0 -19
- package/src/costum/schemas/coFinancementTierslieux.ts +0 -27
- package/src/costum/schemas/coeurNumerique.ts +0 -19
- package/src/costum/schemas/communLundi.ts +0 -19
- package/src/costum/schemas/cressReunion.ts +0 -35
- package/src/costum/schemas/cressReunionNetwork.ts +0 -35
- package/src/costum/schemas/ctenat.ts +0 -43
- package/src/costum/schemas/cyberReunion.ts +0 -19
- package/src/costum/schemas/ekisphere.ts +0 -35
- package/src/costum/schemas/equipementsSportifs974.ts +0 -19
- package/src/costum/schemas/federationDesCae.ts +0 -19
- package/src/costum/schemas/formationGenerique.ts +0 -35
- package/src/costum/schemas/franceTierslieux.ts +0 -19
- package/src/costum/schemas/hinaura.ts +0 -27
- package/src/costum/schemas/hubmayotte.ts +0 -19
- package/src/costum/schemas/hubnouvellecaledonie.ts +0 -19
- package/src/costum/schemas/hubpolynesiefrancaise.ts +0 -19
- package/src/costum/schemas/hubreunion.ts +0 -19
- package/src/costum/schemas/hubterritoires.ts +0 -19
- package/src/costum/schemas/hubwallisetfutuna.ts +0 -19
- package/src/costum/schemas/hva.ts +0 -27
- package/src/costum/schemas/ibv2.ts +0 -19
- package/src/costum/schemas/institutBleu.ts +0 -19
- package/src/costum/schemas/journalInsoumisChambery.ts +0 -27
- package/src/costum/schemas/kpacite.ts +0 -19
- package/src/costum/schemas/laCompagnieDesTierslieux.ts +0 -43
- package/src/costum/schemas/laMaisonDesFaiseurs.ts +0 -19
- package/src/costum/schemas/laReunionPourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/laSemaineDesTierslieux.ts +0 -19
- package/src/costum/schemas/labEnCommuns.ts +0 -19
- package/src/costum/schemas/lakou.ts +0 -27
- package/src/costum/schemas/lesCommunsDesTierslieux.ts +0 -19
- package/src/costum/schemas/mayottePourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/meir.ts +0 -27
- package/src/costum/schemas/navigatorDesTierslieux.ts +0 -19
- package/src/costum/schemas/notragora.ts +0 -27
- package/src/costum/schemas/nouvellecaledoniePourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/openAtlas.ts +0 -19
- package/src/costum/schemas/polynesieFrancaisePourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/schemas/prdr.ts +0 -19
- package/src/costum/schemas/reseauTierslieux.ts +0 -19
- package/src/costum/schemas/rfflabs.ts +0 -19
- package/src/costum/schemas/ries.ts +0 -19
- package/src/costum/schemas/siteDuPactePourLaTransition.ts +0 -27
- package/src/costum/schemas/sportSanteBienetre.ts +0 -43
- package/src/costum/schemas/suppa.ts +0 -19
- package/src/costum/schemas/terramies.ts +0 -19
- package/src/costum/schemas/testeTplFormation.ts +0 -35
- package/src/costum/schemas/tierslieuxbelgique.ts +0 -19
- package/src/costum/schemas/tierslieuxorg1.ts +0 -35
- package/src/costum/schemas/transiter.ts +0 -35
- package/src/costum/schemas/vhelioPeiTour2025.ts +0 -19
- package/src/costum/schemas/wallisetfutunaPourUnNumeriqueInclusif.ts +0 -19
- package/src/costum/symbol.ts +0 -26
- package/src/costum/types.ts +0 -895
- package/src/endpoints.module.ts +0 -4253
- package/src/error.ts +0 -86
- package/src/forms/entityForm.ts +0 -142
- package/src/index.ts +0 -161
- package/src/mixin/UserMixin.ts +0 -4
- package/src/types/api-responses.ts +0 -217
- package/src/types/entities.ts +0 -58
- package/src/types/error-guards.ts +0 -230
- package/src/types/index.ts +0 -41
- package/src/types/payloads.ts +0 -21
- package/src/types/transforms.ts +0 -119
- package/src/utils/FileOfflineStorageStrategy.node.ts +0 -50
- package/src/utils/FileStorageStrategy.node.ts +0 -63
- package/src/utils/MultiServerFileStorageStrategy.node.ts +0 -67
- package/src/utils/MultiServerTokenStorageStrategy.ts +0 -139
- package/src/utils/OfflineClientManager.ts +0 -216
- package/src/utils/OfflineQueueStorageStrategy.ts +0 -47
- package/src/utils/TokenStorage.ts +0 -77
- package/src/utils/compat.ts +0 -12
- package/src/utils/createDefaultMultiServerTokenStorageStrategy.ts +0 -35
- package/src/utils/createDefaultOfflineStrategy.ts +0 -34
- package/src/utils/createDefaultTokenStorageStrategy.ts +0 -33
- package/src/utils/reactive.ts +0 -243
- package/src/utils/stream-utils.node.ts +0 -12
package/src/ApiClient.ts
DELETED
|
@@ -1,1994 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from "events";
|
|
2
|
-
|
|
3
|
-
import { AggregateAjvError } from "@segment/ajv-human-errors";
|
|
4
|
-
import axios from "axios";
|
|
5
|
-
import axiosRetryImport from "axios-retry";
|
|
6
|
-
import { jwtDecode } from "jwt-decode";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { COSTUM_NUMBER_FIELDS, COSTUM_DATE_FIELDS } from "./costum/normalize.js";
|
|
10
|
-
import { COSTUM_REQUEST_ENVELOPE, type CostumRequestEnvelope } from "./costum/symbol.js";
|
|
11
|
-
import MongoID from "./EJSONType.js";
|
|
12
|
-
import endpointsJson from "./endpoints.module.js";
|
|
13
|
-
import { ApiClientError, ApiResponseError, ApiValidationError, CircuitBreakerError } from "./error.js";
|
|
14
|
-
import { getErrorMessage, getErrorStatusCode, hasResponse } from "./types/error-guards.js";
|
|
15
|
-
import { Ajv, addFormats, pino, EJSON } from "./utils/compat.js";
|
|
16
|
-
import { MultiServerTokenStorageStrategy } from "./utils/MultiServerTokenStorageStrategy.js";
|
|
17
|
-
import { MemoryStorageStrategy, type TokenStorageStrategy } from "./utils/TokenStorage.js";
|
|
18
|
-
|
|
19
|
-
import type { ApiErrorDetails } from "./types/api-responses.js";
|
|
20
|
-
import type OfflineClientManager from "./utils/OfflineClientManager.js";
|
|
21
|
-
import type { ValidateFunction } from "ajv";
|
|
22
|
-
import type { AxiosInstance } from "axios";
|
|
23
|
-
import type { JwtPayload } from "jwt-decode";
|
|
24
|
-
|
|
25
|
-
// Fix pour le typage de axios-retry avec les modules CommonJS
|
|
26
|
-
interface AxiosRetryType {
|
|
27
|
-
(axiosInstance: AxiosInstance, config?: any): void;
|
|
28
|
-
exponentialDelay: (retryNumber?: number, error?: Error, delayFactor?: number) => number;
|
|
29
|
-
isNetworkError: (error: Error) => boolean;
|
|
30
|
-
isRetryableError: (error: Error) => boolean;
|
|
31
|
-
}
|
|
32
|
-
const axiosRetry = axiosRetryImport as unknown as AxiosRetryType;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Résultat retourné quand une requête est **mise en file** (offline ou breaker).
|
|
36
|
-
* Permet de typer précisément les chemins « non-réseau » de `callEndpoint`.
|
|
37
|
-
*/
|
|
38
|
-
export interface ApiClientOfflineEnqueueResult {
|
|
39
|
-
data: null;
|
|
40
|
-
offline?: true; // Présent si l'enqueue est dû au mode offline.
|
|
41
|
-
breaker?: true; // Présent si l'enqueue est dû au circuit breaker.
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Options de configuration pour ApiClient
|
|
46
|
-
*/
|
|
47
|
-
export interface ApiClientOptions {
|
|
48
|
-
baseURL: string;
|
|
49
|
-
accessToken?: string | null;
|
|
50
|
-
refreshToken?: string | null;
|
|
51
|
-
refreshUrl?: string;
|
|
52
|
-
endpoints?: any[];
|
|
53
|
-
timeout?: number;
|
|
54
|
-
debug?: boolean;
|
|
55
|
-
maxRetries?: number;
|
|
56
|
-
circuitBreakerThreshold?: number;
|
|
57
|
-
circuitBreakerResetTime?: number;
|
|
58
|
-
fromJSONValue?: boolean;
|
|
59
|
-
/**
|
|
60
|
-
* Mode de dialogue avec le backend cocolight-backend :
|
|
61
|
-
* - `legacy` (défaut) : réponses byte-compatibles PHP ; la lib applique TOUTES ses
|
|
62
|
-
* normalisations côté client (_transformData/_normalizeJsonData) + validation AJV.
|
|
63
|
-
* - `clean` : envoie `X-Coco-Mode: clean` ; le SERVEUR applique les normalisations et
|
|
64
|
-
* répond {success, data|error}. La lib unwrap l'enveloppe, lève ApiResponseError sur
|
|
65
|
-
* success:false, SAUTE _transformData et la validation AJV (schémas legacy), et revive
|
|
66
|
-
* (dates ISO -> Date, id -> _id ObjectID) pour rendre à l'app le MÊME shape qu'avant.
|
|
67
|
-
* NB : l'intercepteur refresh-token reste en legacy (appel interne sans le header).
|
|
68
|
-
*/
|
|
69
|
-
mode?: "legacy" | "clean";
|
|
70
|
-
tokenStorageStrategy?: TokenStorageStrategy | null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Structure des données pour les appels d'endpoint
|
|
75
|
-
*/
|
|
76
|
-
export interface CallEndpointData {
|
|
77
|
-
pathParams?: Record<string, any>;
|
|
78
|
-
[key: string]: any;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Résultat d'un appel d'endpoint réussi
|
|
83
|
-
*/
|
|
84
|
-
export interface CallEndpointResult<T = any> {
|
|
85
|
-
data: T;
|
|
86
|
-
offline?: never;
|
|
87
|
-
breaker?: never;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Union type pour tous les résultats possibles de callEndpoint
|
|
92
|
-
*/
|
|
93
|
-
export type CallEndpointResponse<T = any> = CallEndpointResult<T> | ApiClientOfflineEnqueueResult;
|
|
94
|
-
|
|
95
|
-
EJSON.addType("oid", (value: any) => {
|
|
96
|
-
if (typeof value === "string") {
|
|
97
|
-
return new MongoID.ObjectID(value);
|
|
98
|
-
}
|
|
99
|
-
if (typeof value === "undefined") {
|
|
100
|
-
return new MongoID.ObjectID();
|
|
101
|
-
}
|
|
102
|
-
throw new Error("Expected string value for ObjectID");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Client générique pour consommer une API REST avec validation AJV, gestion des tokens,
|
|
108
|
-
* circuit breaker, retry automatique, et support offline.
|
|
109
|
-
*
|
|
110
|
-
* @fires ApiClient#retryAttempt
|
|
111
|
-
* @fires ApiClient#queuedOffline
|
|
112
|
-
* @fires ApiClient#circuitBreakerOpen
|
|
113
|
-
* @fires ApiClient#circuitBreakerReset
|
|
114
|
-
* @fires ApiClient#refreshSuccess
|
|
115
|
-
* @fires ApiClient#refreshFailed
|
|
116
|
-
* @fires ApiClient#sessionReset
|
|
117
|
-
* @fires ApiClient#validationError
|
|
118
|
-
* @fires ApiClient#offlineModeChanged
|
|
119
|
-
* @fires ApiClient#userLoggedIn
|
|
120
|
-
*/
|
|
121
|
-
export default class ApiClient extends EventEmitter {
|
|
122
|
-
// Public properties
|
|
123
|
-
public readonly __entityTag: string = "ApiClient";
|
|
124
|
-
public readonly userId: string | null = null;
|
|
125
|
-
|
|
126
|
-
// Private properties
|
|
127
|
-
private readonly _baseURL: string;
|
|
128
|
-
private readonly _refreshUrl: string;
|
|
129
|
-
private readonly _endpoints: any[];
|
|
130
|
-
private readonly _debug: boolean;
|
|
131
|
-
private readonly _fromJSONValue: boolean;
|
|
132
|
-
private readonly _tokenStorage: TokenStorageStrategy;
|
|
133
|
-
|
|
134
|
-
private _offlineClientManager: OfflineClientManager | null = null;
|
|
135
|
-
private _ajv: any;
|
|
136
|
-
private _ajvNoDefaults: any;
|
|
137
|
-
public _logger: any;
|
|
138
|
-
private _client: AxiosInstance;
|
|
139
|
-
|
|
140
|
-
// Circuit breaker properties
|
|
141
|
-
private readonly _breakerThreshold: number;
|
|
142
|
-
private readonly _breakerResetTime: number;
|
|
143
|
-
private _breakerErrorCount: number = 0;
|
|
144
|
-
private _breakerOpen: boolean = false;
|
|
145
|
-
private _lastBreakerOpenTime: number | null = null;
|
|
146
|
-
|
|
147
|
-
// Token properties
|
|
148
|
-
private _accessToken: string | null = null;
|
|
149
|
-
private _refreshToken: string | null = null;
|
|
150
|
-
|
|
151
|
-
// Internal userId setter
|
|
152
|
-
private _setUserId: (id: string | null) => void;
|
|
153
|
-
private _mode: "legacy" | "clean" = "legacy";
|
|
154
|
-
constructor({
|
|
155
|
-
baseURL,
|
|
156
|
-
accessToken,
|
|
157
|
-
refreshToken,
|
|
158
|
-
refreshUrl = "/api/cocolight/refreshtoken",
|
|
159
|
-
endpoints = endpointsJson.endpoints,
|
|
160
|
-
timeout = 30000,
|
|
161
|
-
debug = false,
|
|
162
|
-
maxRetries = 0,
|
|
163
|
-
circuitBreakerThreshold = 5,
|
|
164
|
-
circuitBreakerResetTime = 60000,
|
|
165
|
-
fromJSONValue = true,
|
|
166
|
-
mode = "legacy",
|
|
167
|
-
tokenStorageStrategy = null
|
|
168
|
-
}: ApiClientOptions) {
|
|
169
|
-
super(); // EventEmitter
|
|
170
|
-
|
|
171
|
-
if (!baseURL) {
|
|
172
|
-
throw new ApiClientError("Le paramètre \"baseURL\" est obligatoire.", 500);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
this._baseURL = baseURL;
|
|
176
|
-
this._refreshUrl = refreshUrl;
|
|
177
|
-
this._endpoints = endpoints || endpointsJson.endpoints;
|
|
178
|
-
this._debug = debug;
|
|
179
|
-
this._fromJSONValue = fromJSONValue;
|
|
180
|
-
this._mode = mode === "clean" ? "clean" : "legacy";
|
|
181
|
-
this._breakerThreshold = circuitBreakerThreshold;
|
|
182
|
-
this._breakerResetTime = circuitBreakerResetTime;
|
|
183
|
-
|
|
184
|
-
// Setup userId with getter/setter pattern
|
|
185
|
-
let _userId: string | null = null;
|
|
186
|
-
|
|
187
|
-
Object.defineProperty(this, "userId", {
|
|
188
|
-
get: () => _userId,
|
|
189
|
-
set: () => {
|
|
190
|
-
throw new Error("Modification directe de userId non autorisée.");
|
|
191
|
-
},
|
|
192
|
-
enumerable: true
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
this._setUserId = (id: string | null) => {
|
|
196
|
-
_userId = id;
|
|
197
|
-
this._logger?.debug(`[ApiClient] userId set: ${id}`);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
// AJV
|
|
201
|
-
this._ajv = new Ajv({ strict: false, useDefaults: true, allErrors: true, verbose: true });
|
|
202
|
-
// Variante SANS injection de défauts : utilisée pour l'UPDATE costum via `element/save`
|
|
203
|
-
// (enveloppe.isUpdate) — on n'envoie que les champs modifiés, jamais les défauts de création.
|
|
204
|
-
this._ajvNoDefaults = new Ajv({ strict: false, useDefaults: false, allErrors: true, verbose: true });
|
|
205
|
-
for (const ajv of [this._ajv, this._ajvNoDefaults]) {
|
|
206
|
-
addFormats(ajv);
|
|
207
|
-
ajv.addFormat("textarea", true);
|
|
208
|
-
ajv.addKeyword({
|
|
209
|
-
keyword: "startBeforeEnd",
|
|
210
|
-
type: "object",
|
|
211
|
-
errors: true,
|
|
212
|
-
validate: this._startBeforeEndValidate
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Pino logger
|
|
217
|
-
this._logger = pino({
|
|
218
|
-
transport: {
|
|
219
|
-
target: "pino-pretty",
|
|
220
|
-
options: { colorize: true }
|
|
221
|
-
},
|
|
222
|
-
level: debug ? "debug" : "info"
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Création instance Axios
|
|
226
|
-
this._client = axios.create({
|
|
227
|
-
baseURL,
|
|
228
|
-
timeout
|
|
229
|
-
});
|
|
230
|
-
// axios-retry : pour retenter en cas de soucis réseau ou code 5xx
|
|
231
|
-
if (maxRetries > 0) {
|
|
232
|
-
axiosRetry(this._client, {
|
|
233
|
-
retries: maxRetries,
|
|
234
|
-
retryDelay: axiosRetry.exponentialDelay,
|
|
235
|
-
retryCondition: (error: any) => {
|
|
236
|
-
// Retry sur erreurs 5xx ou erreurs réseau
|
|
237
|
-
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
|
238
|
-
},
|
|
239
|
-
onRetry: (retryCount: number, error: any, requestConfig: any) => {
|
|
240
|
-
this._logger.warn(`[Retry] Tentative #${retryCount} pour ${requestConfig?.url}`);
|
|
241
|
-
this.emit("retryAttempt", { retryCount, url: requestConfig?.url });
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
this._logger.info(`[ApiClient] Retry activé : ${maxRetries} max`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (tokenStorageStrategy instanceof MultiServerTokenStorageStrategy) {
|
|
248
|
-
tokenStorageStrategy.use(this._baseURL);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
this._tokenStorage = tokenStorageStrategy || new MemoryStorageStrategy();
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
typeof this._tokenStorage?.getAccessToken !== "function" ||
|
|
255
|
-
typeof this._tokenStorage?.setAccessToken !== "function" ||
|
|
256
|
-
typeof this._tokenStorage?.getRefreshToken !== "function" ||
|
|
257
|
-
typeof this._tokenStorage?.setRefreshToken !== "function" ||
|
|
258
|
-
typeof this._tokenStorage?.clear !== "function"
|
|
259
|
-
) {
|
|
260
|
-
throw new Error("[ApiClient] La stratégie de stockage des tokens n’est pas valide. Elle doit implémenter les méthodes requises.");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Applique un token initial s'il est fourni
|
|
264
|
-
if (refreshToken) {
|
|
265
|
-
this.setRefreshToken(refreshToken);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (accessToken) {
|
|
269
|
-
this.setToken(accessToken);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const initialAccessToken = this._tokenStorage.getAccessToken();
|
|
273
|
-
if (initialAccessToken) {
|
|
274
|
-
this.setToken(initialAccessToken);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const initialRefreshToken = this._tokenStorage.getRefreshToken();
|
|
278
|
-
if (initialRefreshToken) {
|
|
279
|
-
this.setRefreshToken(initialRefreshToken);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// Intercepteur 401 -> refresh
|
|
284
|
-
this._client.interceptors.response.use(
|
|
285
|
-
response => response,
|
|
286
|
-
async error => {
|
|
287
|
-
const originalRequest = error.config;
|
|
288
|
-
|
|
289
|
-
// Si la requête est déjà réessayée, abandonne
|
|
290
|
-
if (originalRequest._retry) {
|
|
291
|
-
this._logger.error("[ApiClient] Requête déjà retentée, échec définitif.");
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (error.response && error.response.status === 401 && this._refreshToken) {
|
|
296
|
-
originalRequest._retry = true;
|
|
297
|
-
|
|
298
|
-
try {
|
|
299
|
-
this._logger.info("[ApiClient] Tentative de refresh du token...");
|
|
300
|
-
const refreshed = await this._refreshAccessToken();
|
|
301
|
-
|
|
302
|
-
if (refreshed) {
|
|
303
|
-
this._logger.info("[ApiClient] Token rafraîchi avec succès.");
|
|
304
|
-
|
|
305
|
-
// 🔑 Mise à jour EXPLICITE du header Authorization dans la requête originale
|
|
306
|
-
originalRequest.headers = originalRequest.headers ?? {};
|
|
307
|
-
originalRequest.headers["Authorization"] = "Bearer " + this.getToken();
|
|
308
|
-
|
|
309
|
-
this._logger.info("[ApiClient] Retente la requête originale avec le nouveau token.");
|
|
310
|
-
return this._client.request(originalRequest);
|
|
311
|
-
} else {
|
|
312
|
-
this.resetSession();
|
|
313
|
-
throw new ApiClientError("Impossible de rafraîchir le token.", 401);
|
|
314
|
-
}
|
|
315
|
-
} catch (err) {
|
|
316
|
-
this.resetSession();
|
|
317
|
-
const errorDetails: ApiErrorDetails = {
|
|
318
|
-
code: getErrorStatusCode(err) || 401,
|
|
319
|
-
message: getErrorMessage(err)
|
|
320
|
-
};
|
|
321
|
-
throw new ApiClientError("Erreur lors du rafraîchissement du token.", 401, errorDetails);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
throw error;
|
|
326
|
-
}
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Sets the access token for the API client and updates the authorization header.
|
|
333
|
-
*/
|
|
334
|
-
setToken(token: string | null): void {
|
|
335
|
-
this._accessToken = token;
|
|
336
|
-
this._tokenStorage.setAccessToken(token ?? "");
|
|
337
|
-
if (token) {
|
|
338
|
-
this._client.defaults.headers.common["Authorization"] = "Bearer " + token;
|
|
339
|
-
} else {
|
|
340
|
-
delete this._client.defaults.headers.common["Authorization"];
|
|
341
|
-
}
|
|
342
|
-
// Extrait l'id depuis le token et le stocke si disponible
|
|
343
|
-
const userId = this._getIdFromToken(token);
|
|
344
|
-
if (userId) {
|
|
345
|
-
this._setUserId(userId);
|
|
346
|
-
this._logger.debug(`[ApiClient] userId extrait et défini : ${userId}`);
|
|
347
|
-
}
|
|
348
|
-
this._logger.debug(`[ApiClient] setToken: ${token}`);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Retrieves the current access token.
|
|
353
|
-
*/
|
|
354
|
-
getToken(): string | null {
|
|
355
|
-
return this._accessToken;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/**
|
|
359
|
-
* Sets the refresh token for the API client.
|
|
360
|
-
*/
|
|
361
|
-
setRefreshToken(token: string | null): void {
|
|
362
|
-
this._refreshToken = token;
|
|
363
|
-
this._tokenStorage.setRefreshToken(token ?? "");
|
|
364
|
-
if(this.userId === null){
|
|
365
|
-
// Extrait l'id depuis le token et le stocke si disponible
|
|
366
|
-
const userId = this._getIdFromToken(token);
|
|
367
|
-
if (userId) {
|
|
368
|
-
this._setUserId(userId);
|
|
369
|
-
this._logger.debug(`[ApiClient] userId extrait depuis refreshToken : ${userId}`);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
this._logger.debug(`[ApiClient] setRefreshToken: ${token}`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Retrieves the current refresh token.
|
|
377
|
-
*/
|
|
378
|
-
getRefreshToken(): string | null {
|
|
379
|
-
return this._refreshToken;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Indique si le client est connecté.
|
|
384
|
-
* On considère que le client est connecté si un token d'accès (_accessToken) est défini.
|
|
385
|
-
*/
|
|
386
|
-
get isConnected(): boolean {
|
|
387
|
-
return !!this._accessToken;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Extrait l'identifiant depuis un JWT.
|
|
392
|
-
*/
|
|
393
|
-
private _getIdFromToken(token: string | null): string | null {
|
|
394
|
-
if (!token) return null;
|
|
395
|
-
try {
|
|
396
|
-
// Décodage du token grâce à jwt-decode
|
|
397
|
-
const payload = jwtDecode<JwtPayload & { id?: string; userId?: string }>(token);
|
|
398
|
-
// L'identifiant peut être dans "id" ou "userId"
|
|
399
|
-
this._logger.debug("[ApiClient] Payload décodé :", payload);
|
|
400
|
-
return payload.id || payload.userId || null;
|
|
401
|
-
} catch (err) {
|
|
402
|
-
this._logger.error("[ApiClient] Erreur lors du décodage du token :", err);
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/**
|
|
408
|
-
* Méthode simplifiée de refresh (en JSON).
|
|
409
|
-
* Emet un event refreshSuccess si ça marche
|
|
410
|
-
*/
|
|
411
|
-
private async _refreshAccessToken(): Promise<boolean> {
|
|
412
|
-
if (!this._refreshToken) return false;
|
|
413
|
-
|
|
414
|
-
const refreshClient = axios.create({
|
|
415
|
-
baseURL: this._baseURL,
|
|
416
|
-
timeout: 10000, // ajuster si nécessaire
|
|
417
|
-
headers: { "Content-Type": "application/json" }
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
const response = await refreshClient.post(this._refreshUrl, {
|
|
422
|
-
refreshToken: this._refreshToken
|
|
423
|
-
});
|
|
424
|
-
if (response.data && response.data.token) {
|
|
425
|
-
this.setToken(response.data.token);
|
|
426
|
-
if (response.data.refreshToken) {
|
|
427
|
-
this.setRefreshToken(response.data.refreshToken);
|
|
428
|
-
}
|
|
429
|
-
this.emit("refreshSuccess", response.data);
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
return false;
|
|
433
|
-
} catch (err) {
|
|
434
|
-
// Si on a une erreur, on reset la session
|
|
435
|
-
this.emit("refreshFailed", { error: getErrorMessage(err) });
|
|
436
|
-
this.resetSession();
|
|
437
|
-
this._logger.error(`[ApiClient] Refresh Error : ${getErrorMessage(err)}`);
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* checkCircuitBreaker : vérifie si on peut appeler l'API ou non
|
|
444
|
-
* si le breaker est "open", on regarde si on peut "reset"
|
|
445
|
-
*/
|
|
446
|
-
private _checkCircuitBreaker(): boolean {
|
|
447
|
-
if (!this._breakerOpen) return true;
|
|
448
|
-
const now = Date.now();
|
|
449
|
-
if (this._lastBreakerOpenTime != null && (now - this._lastBreakerOpenTime) > this._breakerResetTime) {
|
|
450
|
-
this._breakerOpen = false;
|
|
451
|
-
this._breakerErrorCount = 0;
|
|
452
|
-
this._logger.warn("[ApiClient] Circuit breaker réinitialisé");
|
|
453
|
-
this.emit("circuitBreakerReset");
|
|
454
|
-
return true;
|
|
455
|
-
}
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* updateCircuitBreaker : incremente le compteur d'erreurs
|
|
461
|
-
* si on dépasse le threshold, on "open" le breaker.
|
|
462
|
-
*/
|
|
463
|
-
private _updateCircuitBreakerError(): void {
|
|
464
|
-
this._breakerErrorCount += 1;
|
|
465
|
-
this._logger.warn(`[ApiClient] Erreur #${this._breakerErrorCount} sur ${this._breakerThreshold}`);
|
|
466
|
-
if (this._breakerErrorCount >= this._breakerThreshold) {
|
|
467
|
-
this._breakerOpen = true;
|
|
468
|
-
this._lastBreakerOpenTime = Date.now();
|
|
469
|
-
this._logger.error("[ApiClient] Circuit breaker ACTIVÉ - L'API est considérée indisponible");
|
|
470
|
-
this.emit("circuitBreakerOpen", { timestamp: this._lastBreakerOpenTime });
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* resetCircuitBreaker : en cas de succès on reset le compteur
|
|
476
|
-
*/
|
|
477
|
-
private _resetCircuitBreakerSuccess(): void {
|
|
478
|
-
this._breakerErrorCount = 0;
|
|
479
|
-
if (this._breakerOpen) {
|
|
480
|
-
this._breakerOpen = false;
|
|
481
|
-
this._logger.warn("[ApiClient] Circuit breaker refermé suite à un succès");
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
static stripNullsInPlace(obj: any): any {
|
|
486
|
-
// Si l'objet semble être un "fichier uploadé", c'est-à-dire qu'il possède une propriété "value" qui est un stream, on le laisse intact.
|
|
487
|
-
if (obj && typeof obj === "object" && obj.value && typeof obj.value.pipe === "function") {
|
|
488
|
-
return obj;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (Array.isArray(obj)) {
|
|
492
|
-
for (let i = obj.length - 1; i >= 0; i--) {
|
|
493
|
-
if (obj[i] === null || obj[i] === undefined) {
|
|
494
|
-
obj.splice(i, 1);
|
|
495
|
-
} else if (typeof obj[i] === "object") {
|
|
496
|
-
ApiClient.stripNullsInPlace(obj[i]);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
} else if (obj && typeof obj === "object") {
|
|
500
|
-
for (const key in obj) {
|
|
501
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
502
|
-
if (obj[key] === null || obj[key] === undefined) {
|
|
503
|
-
delete obj[key];
|
|
504
|
-
} else if (typeof obj[key] === "object") {
|
|
505
|
-
ApiClient.stripNullsInPlace(obj[key]);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return obj;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
private _resolveSpecialValuesInPlace(obj: any, pathParams: any = {}) {
|
|
514
|
-
const aliasMap: any = {
|
|
515
|
-
userId: () => this.userId,
|
|
516
|
-
accessToken: () => this._accessToken,
|
|
517
|
-
refreshToken: () => this._refreshToken,
|
|
518
|
-
baseURL: () => this._baseURL,
|
|
519
|
-
pathParams: (subPath: any) => {
|
|
520
|
-
// Si la valeur existe dans pathParams, on la retourne
|
|
521
|
-
const value = this._getValueByPath(pathParams, subPath);
|
|
522
|
-
return value !== undefined && value !== null ? value : undefined;
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
// Expression régulière qui capture les alias sous forme délimitée @{...} ou simple @...
|
|
527
|
-
const regex = /@(?:\{([^}]+)\}|([\w.]+))/g;
|
|
528
|
-
|
|
529
|
-
const resolveString = (str: any) => {
|
|
530
|
-
if (typeof str !== "string") return str;
|
|
531
|
-
return str.replace(regex, (_, delimitedAlias, plainAlias) => {
|
|
532
|
-
// Si alias délimité, on traite toute la chaîne à l'intérieur des accolades.
|
|
533
|
-
if (delimitedAlias) {
|
|
534
|
-
// On s'attend à une forme comme "pathParams.id.autre"
|
|
535
|
-
if (delimitedAlias.startsWith("pathParams.")) {
|
|
536
|
-
const subPath = delimitedAlias.substring("pathParams.".length);
|
|
537
|
-
const value = aliasMap.pathParams(subPath);
|
|
538
|
-
return value !== undefined && value !== null ? value : `@{${delimitedAlias}}`;
|
|
539
|
-
} else {
|
|
540
|
-
// Pour d'autres alias délimités, on cherche directement dans aliasMap
|
|
541
|
-
const fn = aliasMap[delimitedAlias];
|
|
542
|
-
if (typeof fn === "function") {
|
|
543
|
-
const value = fn();
|
|
544
|
-
return value !== undefined && value !== null ? value : `@{${delimitedAlias}}`;
|
|
545
|
-
}
|
|
546
|
-
return `@{${delimitedAlias}}`;
|
|
547
|
-
}
|
|
548
|
-
} else if (plainAlias) {
|
|
549
|
-
// Traitement de l'alias simple, potentiellement avec des segments
|
|
550
|
-
// Vérifier s'il commence par "pathParams."
|
|
551
|
-
if (plainAlias.startsWith("pathParams.")) {
|
|
552
|
-
// Extrait la partie après "pathParams." et sépare le premier segment et le reste.
|
|
553
|
-
const match = plainAlias.match(/^pathParams\.([^.]+)(.*)$/);
|
|
554
|
-
if (match) {
|
|
555
|
-
const subKey = match[1]; // ex: "id"
|
|
556
|
-
const remainder = match[2]; // ex: "" ou ".autrechose"
|
|
557
|
-
// Si le reste est vide, ou s'il ne doit pas être remplacé (on remplace toujours le premier segment uniquement)
|
|
558
|
-
const value = aliasMap.pathParams(subKey);
|
|
559
|
-
return value !== undefined && value !== null ? value + remainder : `@${plainAlias}`;
|
|
560
|
-
}
|
|
561
|
-
return `@${plainAlias}`;
|
|
562
|
-
} else {
|
|
563
|
-
// Pour tout autre alias simple sans point ou autre
|
|
564
|
-
const fn = aliasMap[plainAlias];
|
|
565
|
-
if (typeof fn === "function") {
|
|
566
|
-
const value = fn();
|
|
567
|
-
return value !== undefined && value !== null ? value : `@${plainAlias}`;
|
|
568
|
-
}
|
|
569
|
-
return `@${plainAlias}`;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return str;
|
|
573
|
-
});
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
// Vérifie si l'objet est un binaire (par exemple un flux ou un Buffer)
|
|
578
|
-
const isBinary = (input: any) => {
|
|
579
|
-
return input && typeof input === "object" &&
|
|
580
|
-
(typeof input.pipe === "function" || (typeof Buffer !== "undefined" && input instanceof Buffer));
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
// Vérifie si l'objet est un "plain object"
|
|
584
|
-
const isPlainObject = (input: any) => {
|
|
585
|
-
return Object.prototype.toString.call(input) === "[object Object]";
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
const internal = (input: any): any => {
|
|
589
|
-
// Si c'est un binaire, ne rien modifier
|
|
590
|
-
if (isBinary(input)) {
|
|
591
|
-
return input;
|
|
592
|
-
}
|
|
593
|
-
// Si c'est un tableau, on traite chaque élément
|
|
594
|
-
if (Array.isArray(input)) {
|
|
595
|
-
return input.map(internal);
|
|
596
|
-
}
|
|
597
|
-
// Si c'est un plain object, on crée un nouvel objet avec remplacement dans les clés et valeurs
|
|
598
|
-
if (isPlainObject(input)) {
|
|
599
|
-
const result: any = {};
|
|
600
|
-
for (const [key, value] of Object.entries(input)) {
|
|
601
|
-
const resolvedKey = resolveString(key);
|
|
602
|
-
result[resolvedKey] = internal(value);
|
|
603
|
-
}
|
|
604
|
-
return result;
|
|
605
|
-
}
|
|
606
|
-
// Si c'est une string, on applique le remplacement
|
|
607
|
-
if (typeof input === "string") {
|
|
608
|
-
return resolveString(input);
|
|
609
|
-
}
|
|
610
|
-
// Sinon (nombre, booléen, etc.), on retourne la valeur telle quelle
|
|
611
|
-
return input;
|
|
612
|
-
};
|
|
613
|
-
|
|
614
|
-
return internal(obj);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
private _cleanSchemaLeftoverAlias(obj: any): any {
|
|
619
|
-
// Expression régulière qui détecte n'importe quel alias du type "@quelqueChose"
|
|
620
|
-
const aliasRegex = /@\w+/;
|
|
621
|
-
|
|
622
|
-
if (Array.isArray(obj)) {
|
|
623
|
-
return obj.map((item: any) => this._cleanSchemaLeftoverAlias(item));
|
|
624
|
-
} else if (obj && typeof obj === "object") {
|
|
625
|
-
const newObj: any = {};
|
|
626
|
-
for (const key of Object.keys(obj)) {
|
|
627
|
-
const val = obj[key];
|
|
628
|
-
|
|
629
|
-
// Si le nom de la clé contient un alias (exemple : "@userId", "@anything"), on ne la copie pas
|
|
630
|
-
if (aliasRegex.test(key)) {
|
|
631
|
-
continue;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// Si la propriété s'appelle "default"
|
|
635
|
-
if (key === "default") {
|
|
636
|
-
// Si la valeur est une chaîne et qu'elle contient un alias, on ne la copie pas
|
|
637
|
-
if (typeof val === "string" && aliasRegex.test(val)) {
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
// Si la valeur est un objet, on le nettoie récursivement
|
|
641
|
-
if (val && typeof val === "object") {
|
|
642
|
-
const cleanedDefault = this._cleanSchemaLeftoverAlias(val);
|
|
643
|
-
// On ajoute seulement si l'objet nettoyé n'est pas vide
|
|
644
|
-
if (Object.keys(cleanedDefault).length > 0) {
|
|
645
|
-
newObj[key] = cleanedDefault;
|
|
646
|
-
}
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// Si la valeur est une chaîne contenant un alias, on ne la copie pas
|
|
652
|
-
if (typeof val === "string" && aliasRegex.test(val)) {
|
|
653
|
-
continue;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Sinon, on nettoie récursivement la valeur
|
|
657
|
-
newObj[key] = this._cleanSchemaLeftoverAlias(val);
|
|
658
|
-
}
|
|
659
|
-
return newObj;
|
|
660
|
-
}
|
|
661
|
-
return obj;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Safely calls an asynchronous function and handles any errors that occur.
|
|
666
|
-
* Logs the error message using the instance's logger before re-throwing the error.
|
|
667
|
-
*
|
|
668
|
-
* @param fn - The asynchronous function to be called.
|
|
669
|
-
* @param args - The arguments to pass to the function.
|
|
670
|
-
* @returns The result of the asynchronous function.
|
|
671
|
-
* @throws {Error} Re-throws any error that occurs during the function execution.
|
|
672
|
-
*/
|
|
673
|
-
async safeCall(fn: (...args: any[]) => Promise<any>, ...args: any[]) {
|
|
674
|
-
try {
|
|
675
|
-
return await fn(...args);
|
|
676
|
-
} catch (error) {
|
|
677
|
-
this._logger.error(`[ApiClient.safeCall] Erreur: ${getErrorMessage(error)}`);
|
|
678
|
-
throw error;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/**
|
|
683
|
-
* Appelle un endpoint avec validations AJV, auth et circuit breaker.
|
|
684
|
-
* En cas d’indisponibilité (offline ou breaker), **met en file** l’action via
|
|
685
|
-
* `_offlineClientManager` si présent.
|
|
686
|
-
*
|
|
687
|
-
* @param constant - The constant representing the endpoint to call.
|
|
688
|
-
* @param data - Données envoyées (incluant éventuellement `pathParams`).
|
|
689
|
-
* @param transformResponseData - `true` (transforme via `_transformData`), `false`, ou une fonction `(data) => any`.
|
|
690
|
-
* @param validateResponseSchema - Whether to validate the response schema.
|
|
691
|
-
* @returns The response from the endpoint call, or an enqueue result if offline or breaker is active.
|
|
692
|
-
* @throws {CircuitBreakerError} If the circuit breaker is activated.
|
|
693
|
-
* @throws {ApiClientError} If the endpoint is not found, token is required but not provided, or validation fails.
|
|
694
|
-
*/
|
|
695
|
-
async callEndpoint<T = any>(
|
|
696
|
-
constant: string,
|
|
697
|
-
data: CallEndpointData = {},
|
|
698
|
-
transformResponseData: boolean | ((data: any) => any) = true,
|
|
699
|
-
validateResponseSchema: boolean = true
|
|
700
|
-
): Promise<CallEndpointResponse<T>> {
|
|
701
|
-
|
|
702
|
-
const endpoint = this._endpoints.find((ep) => ep.constant === constant);
|
|
703
|
-
if (!endpoint) {
|
|
704
|
-
throw new ApiClientError(`Endpoint introuvable : ${constant}`, 404);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const {
|
|
708
|
-
path,
|
|
709
|
-
method,
|
|
710
|
-
contentType,
|
|
711
|
-
auth,
|
|
712
|
-
pathParams: pathSchema,
|
|
713
|
-
request: requestSchema,
|
|
714
|
-
responses
|
|
715
|
-
} = endpoint;
|
|
716
|
-
|
|
717
|
-
const lowerMethod = (method || "GET").toLowerCase();
|
|
718
|
-
const realContentType = contentType || "application/json";
|
|
719
|
-
const headers: Record<string, string> = { "Content-Type": realContentType };
|
|
720
|
-
if (this._mode === "clean") {
|
|
721
|
-
headers["X-Coco-Mode"] = "clean";
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
// Auth headers
|
|
725
|
-
if (this._accessToken) {
|
|
726
|
-
if (auth === "bearer") {
|
|
727
|
-
headers["Authorization"] = `Bearer ${this._accessToken}`;
|
|
728
|
-
} else if (!auth || auth === "none") {
|
|
729
|
-
if (!headers["Authorization"]) {
|
|
730
|
-
headers["Authorization"] = `Bearer ${this._accessToken}`;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
} else if (auth === "bearer") {
|
|
734
|
-
throw new ApiClientError(`Token requis pour l'endpoint sécurisé : ${constant}`, 401);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// === 1. PathParams ===
|
|
738
|
-
let resolvedPath = path;
|
|
739
|
-
let resolvedParams = {};
|
|
740
|
-
if (pathSchema) {
|
|
741
|
-
|
|
742
|
-
// Si auth est "none" et que userId n'est pas défini, on nettoie le schéma
|
|
743
|
-
let schemaToCompile = pathSchema;
|
|
744
|
-
if (auth === "none" && this.userId === null) {
|
|
745
|
-
schemaToCompile = this._cleanSchemaLeftoverAlias(pathSchema);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const pathParams = data.pathParams || {};
|
|
749
|
-
const validatePathParams: ValidateFunction = this._ajv.compile(schemaToCompile);
|
|
750
|
-
const valid = validatePathParams(pathParams);
|
|
751
|
-
|
|
752
|
-
if (!valid) {
|
|
753
|
-
const errorMessages = this._ajvErrorHuman(validatePathParams.errors ?? []);
|
|
754
|
-
this.emit("validationError", { stage: "pathParams", errors: validatePathParams.errors });
|
|
755
|
-
throw new ApiValidationError(`callEndpoint: ${constant} - Path parameter validation failed.`, 400, errorMessages, validatePathParams.errors);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
resolvedParams = this._resolveSpecialValuesInPlace(pathParams);
|
|
759
|
-
|
|
760
|
-
resolvedPath = resolvedPath.replace(/\{(\w+)\}/g, (_: any, key: string) => {
|
|
761
|
-
const val = (resolvedParams as any)[key];
|
|
762
|
-
if (val !== undefined) return encodeURIComponent(val);
|
|
763
|
-
throw new ApiClientError(`Path param manquant ou non résolu : {${key}}`, 400);
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// === 2. Validation données (request schema) ===
|
|
768
|
-
if (requestSchema) {
|
|
769
|
-
// Si auth est "none" et que userId n'est pas défini, on nettoie le schéma
|
|
770
|
-
let schemaToCompile = requestSchema;
|
|
771
|
-
if (auth === "none" && this.userId === null) {
|
|
772
|
-
schemaToCompile = this._cleanSchemaLeftoverAlias(requestSchema);
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Enveloppe costum (canal Symbol, cf. CostumScope) — injection UNIFIÉE données + contexte :
|
|
776
|
-
// - `properties` : champs DATA costum (per-costum) fusionnés dans le schéma racine compilé
|
|
777
|
-
// (`additionalProperties:false` ne voit que les properties du même objet schema, pas les allOf) ;
|
|
778
|
-
// - `context` : costumSlug/costumId/costumType acceptés par l'AJV (props) PUIS injectés dans le payload
|
|
779
|
-
// plus bas → le contrat n'a plus à déclarer le contexte costum.
|
|
780
|
-
const costumEnv = (data as Record<symbol, unknown>)[COSTUM_REQUEST_ENVELOPE] as CostumRequestEnvelope | undefined;
|
|
781
|
-
if (costumEnv) {
|
|
782
|
-
const ctxProps = Object.fromEntries(Object.keys(costumEnv.context ?? {}).map((k) => [k, { type: "string" }]));
|
|
783
|
-
if (costumEnv.isUpdate) {
|
|
784
|
-
// UPDATE costum via `element/save` (pendant de l'ADD) : on n'envoie que les champs MODIFIÉS
|
|
785
|
-
// (standard + costum, schémas portés dans `properties`). On compile un schéma PERMISSIF — pas
|
|
786
|
-
// les `required`/`default` du schéma de création (sinon échec sur `name`/`type`/… non envoyés et
|
|
787
|
-
// réinjection de `parent` par défaut). $defs conservés pour les éventuels $ref des properties.
|
|
788
|
-
schemaToCompile = {
|
|
789
|
-
type: "object",
|
|
790
|
-
$defs: { ...((schemaToCompile as { $defs?: Record<string, unknown> }).$defs ?? {}), ...(costumEnv.defs ?? {}) },
|
|
791
|
-
properties: { ...(costumEnv.properties ?? {}), ...ctxProps, id: { type: "string" }, collection: { type: "string" } },
|
|
792
|
-
additionalProperties: false,
|
|
793
|
-
};
|
|
794
|
-
} else {
|
|
795
|
-
schemaToCompile = {
|
|
796
|
-
...schemaToCompile,
|
|
797
|
-
properties: { ...(schemaToCompile.properties ?? {}), ...(costumEnv.properties ?? {}), ...ctxProps },
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
let dataForValidation = { ...data };
|
|
803
|
-
delete dataForValidation.pathParams;
|
|
804
|
-
delete (dataForValidation as Record<symbol, unknown>)[COSTUM_REQUEST_ENVELOPE];
|
|
805
|
-
// contexte costum injecté dans le payload (auto) → lu par resolveActiveCostum côté backend.
|
|
806
|
-
if (costumEnv?.context) Object.assign(dataForValidation, costumEnv.context);
|
|
807
|
-
|
|
808
|
-
// Normalize Dates to ISO strings for x-www-form-urlencoded BEFORE validation
|
|
809
|
-
if (realContentType === "application/x-www-form-urlencoded") {
|
|
810
|
-
dataForValidation = ApiClient.normalizeDatesForValidation(dataForValidation);
|
|
811
|
-
// For fields with format:"date", extract only YYYY-MM-DD
|
|
812
|
-
dataForValidation = ApiClient.normalizeSchemaDateFields(dataForValidation, schemaToCompile);
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const cleanedData = ApiClient.stripNullsInPlace(dataForValidation);
|
|
816
|
-
|
|
817
|
-
// UPDATE costum (enveloppe.isUpdate) : AJV SANS useDefaults — ne réinjecte aucun défaut de création.
|
|
818
|
-
const requestAjv = costumEnv?.isUpdate ? this._ajvNoDefaults : this._ajv;
|
|
819
|
-
const validateRequest: ValidateFunction = requestAjv.compile(schemaToCompile);
|
|
820
|
-
const valid = validateRequest(cleanedData);
|
|
821
|
-
if (!valid) {
|
|
822
|
-
const errorMessages = validateRequest.errors ? this._ajvErrorHuman(validateRequest.errors) : [];
|
|
823
|
-
this.emit("validationError", { stage: "request", errors: validateRequest.errors });
|
|
824
|
-
throw new ApiValidationError(`callEndpoint: ${constant} - Request validation failed.`, 400, errorMessages , validateRequest.errors);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
data = this._resolveSpecialValuesInPlace(cleanedData, resolvedParams);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// === 3. ENQUEUE SI OFFLINE (après validations, avant le breaker)
|
|
831
|
-
if (this._offlineClientManager?.isOffline()) {
|
|
832
|
-
if (typeof this._offlineClientManager._enqueueOfflineAction === "function") {
|
|
833
|
-
this._logger.warn(`[ApiClient] Mode dégradé : mise en file de ${constant}`);
|
|
834
|
-
await this._offlineClientManager._enqueueOfflineAction({
|
|
835
|
-
constant,
|
|
836
|
-
data,
|
|
837
|
-
options: { transformResponseData, validateResponseSchema }
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
this._logger.info(`[ApiClient] Requête ${constant} mise en file (offline mode)`);
|
|
841
|
-
|
|
842
|
-
this.emit("queuedOffline", {
|
|
843
|
-
constant,
|
|
844
|
-
reason: "offlineMode",
|
|
845
|
-
data
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
const enqueued = { data: null, offline: true as const };
|
|
849
|
-
return enqueued;
|
|
850
|
-
} else {
|
|
851
|
-
this._logger.warn("[ApiClient] Mode dégradé actif mais offlineManager non initialisé correctement");
|
|
852
|
-
throw new ApiClientError("Mode hors-ligne actif, mais gestionnaire offline non disponible.", 503);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// === 4. Circuit Breaker
|
|
857
|
-
if (!this._checkCircuitBreaker()) {
|
|
858
|
-
if (
|
|
859
|
-
this._offlineClientManager?.isOffline?.() === false && // pas déjà en mode offline
|
|
860
|
-
typeof this._offlineClientManager._enqueueOfflineAction === "function"
|
|
861
|
-
) {
|
|
862
|
-
this._logger.warn(`[ApiClient] Circuit breaker actif → mise en file de ${constant}`);
|
|
863
|
-
await this._offlineClientManager._enqueueOfflineAction({
|
|
864
|
-
constant,
|
|
865
|
-
data,
|
|
866
|
-
options: { transformResponseData, validateResponseSchema }
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
this._logger.info(`[ApiClient] Requête ${constant} mise en file (circuit breaker)`);
|
|
870
|
-
|
|
871
|
-
this.emit("queuedOffline", {
|
|
872
|
-
constant,
|
|
873
|
-
reason: "circuitBreaker",
|
|
874
|
-
data
|
|
875
|
-
});
|
|
876
|
-
|
|
877
|
-
const enqueued = { data: null, breaker: true as const };
|
|
878
|
-
return enqueued;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
throw new CircuitBreakerError("Le circuit breaker est activé, impossible d'appeler l'API");
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
// === 5. Payload ===
|
|
885
|
-
let payload;
|
|
886
|
-
if (realContentType === "application/json" || realContentType === "multipart/form-data") {
|
|
887
|
-
payload = data;
|
|
888
|
-
} else if (realContentType === "application/x-www-form-urlencoded") {
|
|
889
|
-
payload = ApiClient.toURLSearchParams(data);
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
this._logger.debug(`[ApiClient] callEndpoint: ${constant} -> ${resolvedPath}, method=${lowerMethod}`);
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
const response = await this._client.request({
|
|
896
|
-
url: resolvedPath,
|
|
897
|
-
method: lowerMethod,
|
|
898
|
-
headers,
|
|
899
|
-
[lowerMethod === "get" ? "params" : "data"]: payload
|
|
900
|
-
});
|
|
901
|
-
|
|
902
|
-
if (this._mode === "clean") {
|
|
903
|
-
// Enveloppe serveur {success, data|error} : unwrap + erreur typée. Les normalisations
|
|
904
|
-
// sont faites PAR LE SERVEUR (serializeClean) — pas de _transformData ni d'AJV legacy ici.
|
|
905
|
-
const body = response.data;
|
|
906
|
-
if (body && typeof body === "object" && !Array.isArray(body) && typeof body.success === "boolean") {
|
|
907
|
-
if (body.success === false) {
|
|
908
|
-
throw new ApiResponseError(body?.error?.message ?? "Erreur inconnue", response.status, body);
|
|
909
|
-
}
|
|
910
|
-
response.data = body.data;
|
|
911
|
-
}
|
|
912
|
-
if (this._fromJSONValue) {
|
|
913
|
-
response.data = this._reviveClean(response.data);
|
|
914
|
-
}
|
|
915
|
-
} else if (validateResponseSchema) {
|
|
916
|
-
const statusStr = String(response.status);
|
|
917
|
-
const schema = responses?.[statusStr];
|
|
918
|
-
|
|
919
|
-
if (schema) {
|
|
920
|
-
const validateResponse: ValidateFunction = this._ajv.compile(schema);
|
|
921
|
-
const valid = validateResponse(response.data);
|
|
922
|
-
if (!valid) {
|
|
923
|
-
const errorMessages = this._ajvErrorHuman(validateResponse.errors ?? []);
|
|
924
|
-
this.emit("validationError", { stage: "response", errors: validateResponse.errors });
|
|
925
|
-
throw new ApiValidationError("Response validation failed.", response.status, errorMessages, validateResponse.errors);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
this._resetCircuitBreakerSuccess();
|
|
931
|
-
|
|
932
|
-
if (typeof transformResponseData === "function") {
|
|
933
|
-
response.data = transformResponseData(response.data);
|
|
934
|
-
} else if (transformResponseData === true && this._mode !== "clean") {
|
|
935
|
-
response.data = this._transformData(response.data);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
// postActions éventuelles
|
|
939
|
-
if (Array.isArray(endpoint.postActions)) {
|
|
940
|
-
endpoint.postActions.forEach((action: any) => {
|
|
941
|
-
const value = action.path ? this._getValueByPath(response.data, action.path) : null;
|
|
942
|
-
|
|
943
|
-
switch (action.type) {
|
|
944
|
-
case "setToken":
|
|
945
|
-
this.setToken(value);
|
|
946
|
-
break;
|
|
947
|
-
|
|
948
|
-
case "setRefreshToken":
|
|
949
|
-
this.setRefreshToken(value);
|
|
950
|
-
break;
|
|
951
|
-
|
|
952
|
-
// case "setUserId":
|
|
953
|
-
// this._setUserId(value);
|
|
954
|
-
// break;
|
|
955
|
-
|
|
956
|
-
case "resetSession":
|
|
957
|
-
this.resetSession();
|
|
958
|
-
break;
|
|
959
|
-
|
|
960
|
-
case "emitEvent":
|
|
961
|
-
if (action.event) {
|
|
962
|
-
this.emit(action.event, value);
|
|
963
|
-
this._logger.debug(`[ApiClient] Event émis : ${action.event}`, value);
|
|
964
|
-
}
|
|
965
|
-
break;
|
|
966
|
-
|
|
967
|
-
case "callMethod":
|
|
968
|
-
if (typeof (this as any)[action.method] === "function") {
|
|
969
|
-
(this as any)[action.method]();
|
|
970
|
-
this._logger.debug(`[ApiClient] Méthode appelée : ${action.method}`);
|
|
971
|
-
} else {
|
|
972
|
-
this._logger.warn(`[ApiClient] Méthode inconnue : ${action.method}`);
|
|
973
|
-
}
|
|
974
|
-
break;
|
|
975
|
-
|
|
976
|
-
default:
|
|
977
|
-
this._logger.warn(`[ApiClient] Action inconnue : ${action.type}`);
|
|
978
|
-
}
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
return response;
|
|
984
|
-
} catch (error) {
|
|
985
|
-
if (error instanceof ApiResponseError) {
|
|
986
|
-
// Erreur MÉTIER (enveloppe du mode clean success:false) : même sémantique que le
|
|
987
|
-
// checkAndThrowApiResponseError legacy -> re-throw tel quel, et SANS compter dans le
|
|
988
|
-
// circuit breaker (en legacy ces erreurs arrivent en HTTP 200 et ne l'incrémentent pas).
|
|
989
|
-
throw error;
|
|
990
|
-
}
|
|
991
|
-
this._updateCircuitBreakerError();
|
|
992
|
-
this._logger.error(`[ApiClient] Erreur lors de l'appel de ${constant}: ${getErrorMessage(error)}`);
|
|
993
|
-
if(error instanceof ApiValidationError) {
|
|
994
|
-
throw error;
|
|
995
|
-
} else {
|
|
996
|
-
const statusCode = hasResponse(error) ? error.response.status : 500;
|
|
997
|
-
const responseData = hasResponse(error) ? error.response.data : null;
|
|
998
|
-
throw new ApiClientError(
|
|
999
|
-
`Erreur lors de l'appel de l'API : ${getErrorMessage(error)}`,
|
|
1000
|
-
statusCode,
|
|
1001
|
-
responseData
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Converts AJV (Another JSON Schema Validator) errors into human-readable messages.
|
|
1010
|
-
*
|
|
1011
|
-
* @param errors - An array of AJV validation error objects.
|
|
1012
|
-
* @returns An array of human-readable error messages extracted from the AJV errors.
|
|
1013
|
-
*/
|
|
1014
|
-
private _ajvErrorHuman(errors: any[]){
|
|
1015
|
-
try {
|
|
1016
|
-
const errorsMessages = new AggregateAjvError(errors);
|
|
1017
|
-
// @ts-expect-error - champ non typé public dans les defs
|
|
1018
|
-
const messages = errorsMessages.errors.map(({ message }: any) => message);
|
|
1019
|
-
return messages;
|
|
1020
|
-
} catch (e) {
|
|
1021
|
-
this._logger.error("[ApiClient] _ajvErrorHuman", e);
|
|
1022
|
-
return errors.map(({ message }: any) => message);
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Checks the API response for errors and throws an `ApiResponseError` if any are found.
|
|
1028
|
-
*
|
|
1029
|
-
* This method examines the `response.data` object to determine if it contains
|
|
1030
|
-
* error information. It handles both standard and nested error cases:
|
|
1031
|
-
*
|
|
1032
|
-
* - Standard case: If `data.result` is a boolean and is `false`, an error is thrown.
|
|
1033
|
-
* - Nested case: If `data.resultErrors` exists and contains a `result` property
|
|
1034
|
-
* that is a boolean and is `false`, an error is thrown.
|
|
1035
|
-
*
|
|
1036
|
-
* If no errors are found, the original `response` is returned.
|
|
1037
|
-
*
|
|
1038
|
-
* @param {import("axios").AxiosResponse | { data: any } } response
|
|
1039
|
-
* @returns {import("axios").AxiosResponse | { data: any }}
|
|
1040
|
-
* @throws {ApiResponseError} If an error is detected in the response data.
|
|
1041
|
-
*/
|
|
1042
|
-
checkAndThrowApiResponseError(response: any) {
|
|
1043
|
-
if (!("status" in response)) {
|
|
1044
|
-
return response;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
const data = response.data;
|
|
1048
|
-
if (!data || typeof data !== "object") {
|
|
1049
|
-
return response;
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Cas standard : vérifie si data.result vaut false
|
|
1053
|
-
if (typeof data.result === "boolean" && data.result === false) {
|
|
1054
|
-
throw new ApiResponseError(
|
|
1055
|
-
data.msg || "Erreur inconnue",
|
|
1056
|
-
response.status,
|
|
1057
|
-
data
|
|
1058
|
-
);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
// Cas imbriqué : vérifie si data.resultErrors existe et contient une erreur
|
|
1062
|
-
if (data.resultErrors && typeof data.resultErrors === "object") {
|
|
1063
|
-
if (typeof data.resultErrors.result === "boolean" && data.resultErrors.result === false) {
|
|
1064
|
-
throw new ApiResponseError(
|
|
1065
|
-
data.resultErrors.msg || "Erreur inconnue",
|
|
1066
|
-
response.status,
|
|
1067
|
-
data
|
|
1068
|
-
);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
return response;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
/**
|
|
1076
|
-
* Réinitialise complètement la session de l'utilisateur :
|
|
1077
|
-
* - Token d'accès
|
|
1078
|
-
* - Token de rafraîchissement
|
|
1079
|
-
* - userId
|
|
1080
|
-
* - En-têtes Axios
|
|
1081
|
-
*/
|
|
1082
|
-
resetSession(): void {
|
|
1083
|
-
this.setToken(null);
|
|
1084
|
-
this.setRefreshToken(null);
|
|
1085
|
-
this._setUserId(null);
|
|
1086
|
-
this._tokenStorage.clear();
|
|
1087
|
-
|
|
1088
|
-
// Suppression des en-têtes
|
|
1089
|
-
delete this._client.defaults.headers.common["Authorization"];
|
|
1090
|
-
|
|
1091
|
-
this._logger.info("[ApiClient] Session utilisateur réinitialisée.");
|
|
1092
|
-
this.emit("sessionReset");
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
/**
|
|
1096
|
-
* Retrieves the value from an object based on a dot-separated path.
|
|
1097
|
-
*/
|
|
1098
|
-
private _getValueByPath(obj: any, path: string): any {
|
|
1099
|
-
if (!path) return undefined;
|
|
1100
|
-
const keys = path.split(".");
|
|
1101
|
-
return keys.reduce((acc, key) => acc && acc[key], obj);
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
// Conversions
|
|
1105
|
-
|
|
1106
|
-
/**
|
|
1107
|
-
* Normalizes Date objects to ISO strings for AJV validation.
|
|
1108
|
-
* This preserves the object structure while converting only Dates.
|
|
1109
|
-
* Handles reactive Proxies by unwrapping them first.
|
|
1110
|
-
*
|
|
1111
|
-
* @param obj - The object to normalize
|
|
1112
|
-
* @returns The normalized object with Dates as ISO strings
|
|
1113
|
-
*/
|
|
1114
|
-
static normalizeDatesForValidation(obj: any): any {
|
|
1115
|
-
if (obj === null || obj === undefined) return obj;
|
|
1116
|
-
|
|
1117
|
-
// Unwrap reactive Proxies BEFORE checking for Date
|
|
1118
|
-
if (obj && typeof obj === "object" && obj.__isReactive && obj.__raw) {
|
|
1119
|
-
return ApiClient.normalizeDatesForValidation(obj.__raw);
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
if (obj instanceof Date) {
|
|
1123
|
-
return obj.toISOString().replace(/\.\d{3}Z$/, "+0000");
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
if (Array.isArray(obj)) {
|
|
1127
|
-
return obj.map((item: any) => ApiClient.normalizeDatesForValidation(item));
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
if (typeof obj === "object") {
|
|
1131
|
-
const normalized: any = {};
|
|
1132
|
-
for (const key of Object.keys(obj)) {
|
|
1133
|
-
normalized[key] = ApiClient.normalizeDatesForValidation(obj[key]);
|
|
1134
|
-
}
|
|
1135
|
-
return normalized;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
return obj;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
/**
|
|
1142
|
-
* Normalizes fields with format:"date" in schema to YYYY-MM-DD format.
|
|
1143
|
-
* Extracts only the date part from ISO datetime strings.
|
|
1144
|
-
*
|
|
1145
|
-
* @param data - The data object to normalize
|
|
1146
|
-
* @param schema - The JSON schema containing field definitions
|
|
1147
|
-
* @returns The normalized data with date-only strings for format:"date" fields
|
|
1148
|
-
*/
|
|
1149
|
-
static normalizeSchemaDateFields(data: any, schema: any) {
|
|
1150
|
-
if (!data || !schema || typeof data !== "object") return data;
|
|
1151
|
-
|
|
1152
|
-
const normalized = { ...data };
|
|
1153
|
-
|
|
1154
|
-
// Récupérer les propriétés du schéma (gérer allOf, then, properties, etc.)
|
|
1155
|
-
let properties = schema.properties || {};
|
|
1156
|
-
|
|
1157
|
-
// Si le schéma utilise allOf, parcourir tous les éléments
|
|
1158
|
-
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
1159
|
-
for (const allOfItem of schema.allOf) {
|
|
1160
|
-
// Chercher dans properties directes
|
|
1161
|
-
if (allOfItem.properties) {
|
|
1162
|
-
properties = { ...properties, ...allOfItem.properties };
|
|
1163
|
-
}
|
|
1164
|
-
// Chercher dans if/then/properties
|
|
1165
|
-
if (allOfItem.then && allOfItem.then.properties) {
|
|
1166
|
-
properties = { ...properties, ...allOfItem.then.properties };
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// Aussi vérifier schema.then.properties si présent
|
|
1172
|
-
if (schema.then && schema.then.properties) {
|
|
1173
|
-
properties = { ...properties, ...schema.then.properties };
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Pour chaque propriété ayant format:"date"
|
|
1177
|
-
for (const [key, propDef] of Object.entries(properties)) {
|
|
1178
|
-
const typedPropDef = propDef as any;
|
|
1179
|
-
if (typedPropDef?.format === "date" && normalized[key]) {
|
|
1180
|
-
const value = normalized[key];
|
|
1181
|
-
|
|
1182
|
-
// Si c'est une string ISO datetime, extraire YYYY-MM-DD
|
|
1183
|
-
if (typeof value === "string" && value.includes("T")) {
|
|
1184
|
-
normalized[key] = value.split("T")[0];
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
return normalized;
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
/**
|
|
1193
|
-
* Converts an object to URL search parameters.
|
|
1194
|
-
*
|
|
1195
|
-
* @param obj - The object to be converted to URL search parameters.
|
|
1196
|
-
* @param [options={}] - Optional settings for the conversion.
|
|
1197
|
-
* @returns The URL search parameters generated from the object.
|
|
1198
|
-
*/
|
|
1199
|
-
static toURLSearchParams(obj: any, options: any = {}) {
|
|
1200
|
-
return this._buildParams(obj, new URLSearchParams(), options) as URLSearchParams;
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
/**
|
|
1204
|
-
* Builds parameters for an API request from a given object.
|
|
1205
|
-
*/
|
|
1206
|
-
static _buildParams(
|
|
1207
|
-
obj: any,
|
|
1208
|
-
paramsInstance: FormData | URLSearchParams,
|
|
1209
|
-
options: {
|
|
1210
|
-
dots?: boolean;
|
|
1211
|
-
indexes?: boolean;
|
|
1212
|
-
metaTokens?: boolean;
|
|
1213
|
-
} = {}
|
|
1214
|
-
): FormData | URLSearchParams {
|
|
1215
|
-
if (typeof obj !== "object" || obj === null) {
|
|
1216
|
-
throw new TypeError("La donnée doit être un objet non nul.");
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
const { dots = false, indexes = false, metaTokens = true } = options;
|
|
1220
|
-
const stack: any[] = [];
|
|
1221
|
-
|
|
1222
|
-
function isVisitable(thing: any) {
|
|
1223
|
-
return Object.prototype.toString.call(thing) === "[object Object]" || Array.isArray(thing);
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
function removeBrackets(key: any) {
|
|
1227
|
-
return key.endsWith("[]") ? key.slice(0, -2) : key;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
function renderKey(path: any, key: any, useDots = false) {
|
|
1231
|
-
if (!path) return key;
|
|
1232
|
-
return path.concat(key).map((token: any, i: any) => {
|
|
1233
|
-
token = removeBrackets(token);
|
|
1234
|
-
return !useDots && i ? `[${token}]` : token;
|
|
1235
|
-
}).join(useDots ? "." : "");
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
function convertValue(value: any) {
|
|
1239
|
-
if (value === null || value === undefined) return "";
|
|
1240
|
-
if (value instanceof Date) return value.toISOString();
|
|
1241
|
-
if (
|
|
1242
|
-
value instanceof Blob ||
|
|
1243
|
-
value instanceof File ||
|
|
1244
|
-
value instanceof ArrayBuffer ||
|
|
1245
|
-
ArrayBuffer.isView(value)
|
|
1246
|
-
) {
|
|
1247
|
-
return value;
|
|
1248
|
-
}
|
|
1249
|
-
return value;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
function defaultVisitor(value: any, key: any, path: any) {
|
|
1253
|
-
if (value && typeof value === "object") {
|
|
1254
|
-
if (key.endsWith("{}")) {
|
|
1255
|
-
key = metaTokens ? key : key.slice(0, -2);
|
|
1256
|
-
paramsInstance.append(renderKey(path, key, dots), JSON.stringify(value));
|
|
1257
|
-
return false;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
if ((Array.isArray(value) && !value.some(isVisitable)) || key.endsWith("[]")) {
|
|
1261
|
-
key = removeBrackets(key);
|
|
1262
|
-
value.forEach((el: any, index: any) => {
|
|
1263
|
-
if (el !== undefined && el !== null) {
|
|
1264
|
-
// const fieldKey = indexes ? renderKey([key], index, dots) : `${key}[]`;
|
|
1265
|
-
const fieldKey = indexes
|
|
1266
|
-
? renderKey(path.concat(key), index, dots)
|
|
1267
|
-
: `${renderKey(path, key, dots)}[]`;
|
|
1268
|
-
paramsInstance.append(fieldKey, convertValue(el));
|
|
1269
|
-
}
|
|
1270
|
-
});
|
|
1271
|
-
return false;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
if (isVisitable(value)) return true;
|
|
1276
|
-
paramsInstance.append(renderKey(path, key, dots), convertValue(value));
|
|
1277
|
-
return false;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
function build(value: any, path: any = []) {
|
|
1281
|
-
if (value === undefined || value === null || (stack as any).includes(value)) return;
|
|
1282
|
-
(stack as any).push(value);
|
|
1283
|
-
|
|
1284
|
-
Object.entries(value).forEach(([k, el]) => {
|
|
1285
|
-
if (el !== undefined && defaultVisitor(el, k.trim(), path)) {
|
|
1286
|
-
build(el, path.concat(removeBrackets(k)));
|
|
1287
|
-
}
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
stack.pop();
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
build(obj);
|
|
1294
|
-
return paramsInstance;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
/**
|
|
1299
|
-
* Transforms the given data based on its structure.
|
|
1300
|
-
*
|
|
1301
|
-
* This method normalizes various structures of data into a consistent format.
|
|
1302
|
-
* It handles different types of data including objects, arrays, and specific
|
|
1303
|
-
* nested structures like `resultGoods`, `resultErrors`, `results`, `news`,
|
|
1304
|
-
* `notif`, `citoyens`, `organizations`, `cities`, `newComment`, `map`, and `object`.
|
|
1305
|
-
*
|
|
1306
|
-
* @param data - The data to be transformed.
|
|
1307
|
-
* @returns - The transformed data.
|
|
1308
|
-
*
|
|
1309
|
-
* @private
|
|
1310
|
-
*/
|
|
1311
|
-
/**
|
|
1312
|
-
* Reviver du mode clean : redonne à l'app le shape exact qu'elle recevait après les
|
|
1313
|
-
* normalisations legacy de la lib — dates ISO (champs _dateFields) -> instances Date,
|
|
1314
|
-
* `id` oid-hex -> `_id` instance ObjectID (créée via EJSON.fromJSONValue, MÊME chemin
|
|
1315
|
-
* que le mode legacy). Le serveur clean a déjà fait tout le reste.
|
|
1316
|
-
*/
|
|
1317
|
-
private _reviveClean(data: any): any {
|
|
1318
|
-
if (Array.isArray(data)) {
|
|
1319
|
-
return data.map((item) => this._reviveClean(item));
|
|
1320
|
-
}
|
|
1321
|
-
if (data !== null && typeof data === "object") {
|
|
1322
|
-
const out: any = {};
|
|
1323
|
-
Object.keys(data).forEach((key) => {
|
|
1324
|
-
let value = this._reviveClean(data[key]);
|
|
1325
|
-
if (this._dateFields.includes(key) && typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
1326
|
-
const d = new Date(value);
|
|
1327
|
-
if (!Number.isNaN(d.getTime())) value = d;
|
|
1328
|
-
}
|
|
1329
|
-
out[key] = value;
|
|
1330
|
-
});
|
|
1331
|
-
if (typeof out.id === "string" && /^[0-9a-fA-F]{24}$/.test(out.id) && out._id === undefined) {
|
|
1332
|
-
out._id = EJSON.fromJSONValue({ $type: "oid", $value: out.id });
|
|
1333
|
-
}
|
|
1334
|
-
return out;
|
|
1335
|
-
}
|
|
1336
|
-
return data;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
private _transformData(data: any): any {
|
|
1340
|
-
if (data && typeof data === "object") {
|
|
1341
|
-
if (data.resultGoods?.msg) {
|
|
1342
|
-
data = this._normalizeJsonData({ msg: data.resultGoods.msg, ...data });
|
|
1343
|
-
} else if (data.resultErrors?.msg) {
|
|
1344
|
-
data = this._normalizeJsonData({ msg: data.resultErrors.msg, ...data });
|
|
1345
|
-
} else if (data.results && typeof data.results === "object" && !Array.isArray(data.results)) {
|
|
1346
|
-
data.results = Object.keys(data.results).map((key) => {
|
|
1347
|
-
return this._normalizeJsonData({ id: key, ...data.results[key] });
|
|
1348
|
-
});
|
|
1349
|
-
} else if (Array.isArray(data.results) && data.results.length > 0) {
|
|
1350
|
-
data.results = data.results.map((item: any) => this._normalizeJsonData(item));
|
|
1351
|
-
} else if (data.news && Array.isArray(data.news) && data.news.length === 0) {
|
|
1352
|
-
data = data.news;
|
|
1353
|
-
} else if (data.news && typeof data.news === "object" && !Array.isArray(data.news)) {
|
|
1354
|
-
data = Object.keys(data.news).map((key) => {
|
|
1355
|
-
return this._normalizeJsonData({ id: key, ...data.news[key] });
|
|
1356
|
-
});
|
|
1357
|
-
} else if (data.notif && typeof data.notif === "object" && !Array.isArray(data.notif)) {
|
|
1358
|
-
data.notif = Object.keys(data.notif).map((key) => {
|
|
1359
|
-
return this._normalizeJsonData({ id: key, ...data.notif[key] });
|
|
1360
|
-
});
|
|
1361
|
-
} else if (
|
|
1362
|
-
data.citoyens && typeof data.citoyens === "object" && !Array.isArray(data.citoyens) &&
|
|
1363
|
-
data.organizations && typeof data.organizations === "object" && !Array.isArray(data.organizations)
|
|
1364
|
-
) {
|
|
1365
|
-
data = [
|
|
1366
|
-
...Object.keys(data.citoyens).map(key => this._normalizeJsonData({ id: key, ...data.citoyens[key] })),
|
|
1367
|
-
...Object.keys(data.organizations).map(key => this._normalizeJsonData({ id: key, ...data.organizations[key] }))
|
|
1368
|
-
];
|
|
1369
|
-
} else if (
|
|
1370
|
-
data.citoyens && typeof data.citoyens === "object" && !Array.isArray(data.citoyens)
|
|
1371
|
-
) {
|
|
1372
|
-
data = Object.keys(data.citoyens).map(key => this._normalizeJsonData({ id: key, ...data.citoyens[key] }));
|
|
1373
|
-
} else if (data.cities && typeof data.cities === "object" && !Array.isArray(data.cities)) {
|
|
1374
|
-
data = Object.keys(data.cities).map(key => this._normalizeJsonData({ id: key, ...data.cities[key] }));
|
|
1375
|
-
} else if (data.cities && Array.isArray(data.cities) && data.cities.length === 0) {
|
|
1376
|
-
data = data.cities;
|
|
1377
|
-
} else if (data.newComment && typeof data.newComment === "object") {
|
|
1378
|
-
data.newComment = this._normalizeJsonData({ ...data.newComment });
|
|
1379
|
-
} else if (Array.isArray(data)) {
|
|
1380
|
-
data = data.map((item) => this._normalizeJsonData(item));
|
|
1381
|
-
} else if (data.map && typeof data.map === "object") {
|
|
1382
|
-
data.map = this._normalizeJsonData(data.map);
|
|
1383
|
-
} else if (data.object && typeof data.object === "object") {
|
|
1384
|
-
data.object = this._normalizeJsonData(data.object);
|
|
1385
|
-
} else if (typeof data === "object" && Object.keys(data).length > 0) {
|
|
1386
|
-
// Vérifie si toutes les clés sont des IDs mongo
|
|
1387
|
-
const allMongoIds = Object.keys(data).every((key) => /^[0-9a-fA-F]{24}$/.test(key));
|
|
1388
|
-
if (allMongoIds) {
|
|
1389
|
-
data = Object.keys(data).map((key) => ({
|
|
1390
|
-
id: key,
|
|
1391
|
-
...this._normalizeJsonData(data[key])
|
|
1392
|
-
}));
|
|
1393
|
-
} else {
|
|
1394
|
-
data = this._normalizeJsonData(data);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
return data;
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
/**
|
|
1403
|
-
* Normalizes JSON data by transforming specific fields and ensuring URLs are complete.
|
|
1404
|
-
*
|
|
1405
|
-
* @param item - The JSON object to be normalized.
|
|
1406
|
-
* @returns - The normalized JSON object.
|
|
1407
|
-
*
|
|
1408
|
-
* The function performs the following transformations:
|
|
1409
|
-
* - Normalizes the ID field if it matches a specific pattern.
|
|
1410
|
-
* - Converts date fields from seconds to milliseconds.
|
|
1411
|
-
* - Ensures URLs in specific fields are complete.
|
|
1412
|
-
* - Filters and normalizes nested objects and arrays.
|
|
1413
|
-
* - Removes the `timeAgo` field if it exists.
|
|
1414
|
-
* - Converts the object to EJSON format if necessary.
|
|
1415
|
-
*/
|
|
1416
|
-
private _normalizeJsonData(item: any) {
|
|
1417
|
-
if (!item || typeof item !== "object") {
|
|
1418
|
-
return item;
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
// Application des normalisations récursives
|
|
1422
|
-
item = this._normalizeRecursively(item);
|
|
1423
|
-
|
|
1424
|
-
// item = this._normalizeIdRecursively(item);
|
|
1425
|
-
// item = this._normalizeDatesRecursively(item);
|
|
1426
|
-
// item = this._normalizeImagesRecursively(item);
|
|
1427
|
-
|
|
1428
|
-
// if(item?.preferences){
|
|
1429
|
-
// item.preferences = this._normalizeBooleansRecursively(item.preferences);
|
|
1430
|
-
// }
|
|
1431
|
-
|
|
1432
|
-
if (item?.mediaImg?.images && Array.isArray(item.mediaImg.images) && item.mediaImg.images.length > 0) {
|
|
1433
|
-
item.mediaImg.countImages = item.mediaImg.images.length;
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
if (item?.mediaFile?.files && Array.isArray(item.mediaFile.files) && item.mediaFile.files.length > 0) {
|
|
1437
|
-
item.mediaFile.countFiles = item.mediaFile.files.length;
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (
|
|
1441
|
-
item?.src?.changes &&
|
|
1442
|
-
typeof item.src.changes === "object" &&
|
|
1443
|
-
Object.keys(item.src.changes).length > 0
|
|
1444
|
-
) {
|
|
1445
|
-
Object.keys(item.src.changes).forEach((key) => {
|
|
1446
|
-
if (item.src.changes[key]) {
|
|
1447
|
-
item.src.changes[key] = this._ensureFullURL(item.src.changes[key]);
|
|
1448
|
-
}
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
if (item?.openingHours && Array.isArray(item.openingHours) && item.openingHours.length > 0) {
|
|
1453
|
-
item.openingHours = item.openingHours.filter(
|
|
1454
|
-
(day: any) => day.dayOfWeek && day.hours && day.hours[0] && day.hours[0].opens
|
|
1455
|
-
);
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
if (item.replies && typeof item.replies === "object" && Object.keys(item.replies).length > 0) {
|
|
1459
|
-
item.replies = Object.keys(item.replies).map((key) => {
|
|
1460
|
-
return this._normalizeJsonData({ id: key, ...item.replies[key] });
|
|
1461
|
-
});
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
if (
|
|
1465
|
-
item?.author &&
|
|
1466
|
-
typeof item.author === "object" &&
|
|
1467
|
-
Object.keys(item.author).length > 0
|
|
1468
|
-
) {
|
|
1469
|
-
// Si author contient déjà une propriété attendue (ici "profilThumbImageUrl"),
|
|
1470
|
-
// on considère qu'il est déjà aplatit.
|
|
1471
|
-
if (!("profilThumbImageUrl" in item.author)) {
|
|
1472
|
-
// Sinon, on considère que author est un mapping et on prend le premier sous-objet.
|
|
1473
|
-
const firstKey = Object.keys(item.author)[0];
|
|
1474
|
-
// On remplace author par le contenu du sous-objet
|
|
1475
|
-
item.author = { ...item.author[firstKey] };
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
// Supprime timeAgo
|
|
1480
|
-
if (item?.timeAgo) {
|
|
1481
|
-
delete item.timeAgo;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
if(item?.author?.typeSig === "people"){
|
|
1485
|
-
item.author.type = "citoyens";
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
if(item?.typeSig === "people"){
|
|
1489
|
-
item.collection = "citoyens";
|
|
1490
|
-
}
|
|
1491
|
-
if(item?.typeSig === "organizations"){
|
|
1492
|
-
item.collection = "organizations";
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
// Convertit en EJSON si besoin
|
|
1496
|
-
return this._fromJSONValue ? EJSON.fromJSONValue(item) : item;
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
/**
|
|
1501
|
-
* Ensures that the given image path is a full URL. If the image path is already a full URL
|
|
1502
|
-
* (i.e., it starts with "http://" or "https://"), it is returned as is. Otherwise, the image path
|
|
1503
|
-
* is concatenated with the base URL of the ApiClient instance.
|
|
1504
|
-
*
|
|
1505
|
-
* @param imagePath - The image path to ensure as a full URL.
|
|
1506
|
-
* @returns - The full URL of the image path.
|
|
1507
|
-
*/
|
|
1508
|
-
private _ensureFullURL(imagePath: any) {
|
|
1509
|
-
if (!imagePath) return imagePath;
|
|
1510
|
-
imagePath = imagePath.trim();
|
|
1511
|
-
if (!imagePath || /^https?:\/\//i.test(imagePath)) {
|
|
1512
|
-
return imagePath;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
let separator = "/";
|
|
1516
|
-
if (this._baseURL.endsWith("/") && imagePath.startsWith("/")) {
|
|
1517
|
-
separator = "";
|
|
1518
|
-
} else if (!this._baseURL.endsWith("/") && !imagePath.startsWith("/")) {
|
|
1519
|
-
separator = "/";
|
|
1520
|
-
} else {
|
|
1521
|
-
separator = "";
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
return `${this._baseURL}${separator}${imagePath}`;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
/**
|
|
1528
|
-
* Parcourt récursivement un objet pour normaliser les champs de date.
|
|
1529
|
-
* Pour chaque clé présente dans dateFields, si la valeur est un nombre ou un objet
|
|
1530
|
-
* contenant une propriété sec (nombre), la transforme en { $date: valeurEnMs }.
|
|
1531
|
-
*
|
|
1532
|
-
* @param obj - L’objet à normaliser.
|
|
1533
|
-
* @returns L’objet normalisé.
|
|
1534
|
-
*/
|
|
1535
|
-
// _normalizeDatesRecursively(obj) {
|
|
1536
|
-
// if (obj === null || typeof obj !== "object") return obj;
|
|
1537
|
-
|
|
1538
|
-
// // Si c'est un tableau, on traite chaque élément récursivement.
|
|
1539
|
-
// if (Array.isArray(obj)) {
|
|
1540
|
-
// return obj.map(item => this._normalizeDatesRecursively(item));
|
|
1541
|
-
// }
|
|
1542
|
-
|
|
1543
|
-
// Object.keys(obj).forEach((key) => {
|
|
1544
|
-
// if (this._dateFields.includes(key)) {
|
|
1545
|
-
// if (obj[key] && typeof obj[key] === "object" && obj[key].sec && typeof obj[key].sec === "number") {
|
|
1546
|
-
// obj[key] = { $date: obj[key].sec * 1000 };
|
|
1547
|
-
// } else if (typeof obj[key] === "number") {
|
|
1548
|
-
// obj[key] = { $date: obj[key] * 1000 };
|
|
1549
|
-
// }
|
|
1550
|
-
// }
|
|
1551
|
-
// if (obj[key] && typeof obj[key] === "object") {
|
|
1552
|
-
// this._normalizeDatesRecursively(obj[key]);
|
|
1553
|
-
// }
|
|
1554
|
-
// });
|
|
1555
|
-
// return obj;
|
|
1556
|
-
// }
|
|
1557
|
-
|
|
1558
|
-
/**
|
|
1559
|
-
* Parcourt récursivement un objet pour normaliser les chemins d'images.
|
|
1560
|
-
* Pour chaque propriété correspondant à un champ d'image (dans imageFields),
|
|
1561
|
-
* vérifie et convertit le chemin en URL complète à l’aide de _ensureFullURL.
|
|
1562
|
-
* Les cas particuliers (comme mediaImg.images ou mediaFile.files) sont également gérés.
|
|
1563
|
-
*
|
|
1564
|
-
* @param obj - L’objet à normaliser.
|
|
1565
|
-
* @returns L’objet normalisé.
|
|
1566
|
-
*/
|
|
1567
|
-
// _normalizeImagesRecursively(obj) {
|
|
1568
|
-
// if (obj === null || typeof obj !== "object") return obj;
|
|
1569
|
-
|
|
1570
|
-
// // Si c'est un tableau, on traite chaque élément récursivement.
|
|
1571
|
-
// if (Array.isArray(obj)) {
|
|
1572
|
-
// return obj.map(item => this._normalizeImagesRecursively(item));
|
|
1573
|
-
// }
|
|
1574
|
-
|
|
1575
|
-
// Object.keys(obj).forEach((key) => {
|
|
1576
|
-
// if (this._imageFields.includes(key) && typeof obj[key] === "string" && obj[key].trim() !== "") {
|
|
1577
|
-
// obj[key] = this._ensureFullURL(obj[key]);
|
|
1578
|
-
// }
|
|
1579
|
-
|
|
1580
|
-
// // Cas particulier pour content.image
|
|
1581
|
-
// if (key === "content" && obj[key]?.image) {
|
|
1582
|
-
// obj[key].image = this._ensureFullURL(obj[key].image);
|
|
1583
|
-
// }
|
|
1584
|
-
|
|
1585
|
-
// if (obj[key] && typeof obj[key] === "object") {
|
|
1586
|
-
// this._normalizeImagesRecursively(obj[key]);
|
|
1587
|
-
// }
|
|
1588
|
-
// });
|
|
1589
|
-
// return obj;
|
|
1590
|
-
// }
|
|
1591
|
-
|
|
1592
|
-
/**
|
|
1593
|
-
* Parcourt récursivement un objet ou un tableau pour normaliser les identifiants (ID).
|
|
1594
|
-
* Si un objet possède une propriété "_id" ou "id" contenant un sous-objet "$id" valide,
|
|
1595
|
-
* l'ID est normalisé et la propriété _id est convertie au format EJSON attendu.
|
|
1596
|
-
*
|
|
1597
|
-
* @param obj - L'objet ou le tableau à normaliser.
|
|
1598
|
-
* @returns L'objet ou le tableau normalisé.
|
|
1599
|
-
*/
|
|
1600
|
-
// _normalizeIdRecursively(obj) {
|
|
1601
|
-
// if (obj === null || typeof obj !== "object") return obj;
|
|
1602
|
-
|
|
1603
|
-
// // Si c'est un tableau, on traite chaque élément récursivement.
|
|
1604
|
-
// if (Array.isArray(obj)) {
|
|
1605
|
-
// return obj.map(item => this._normalizeIdRecursively(item));
|
|
1606
|
-
// }
|
|
1607
|
-
|
|
1608
|
-
// // Vérifier et normaliser l'ID dans l'objet
|
|
1609
|
-
// if (obj._id && obj._id.$id && /^[0-9a-fA-F]{24}$/.test(obj._id.$id)) {
|
|
1610
|
-
// obj.id = obj._id.$id;
|
|
1611
|
-
// obj._id = { $type: "oid", $value: obj._id.$id };
|
|
1612
|
-
// }
|
|
1613
|
-
// if (obj.id && obj.id.$id && /^[0-9a-fA-F]{24}$/.test(obj.id.$id)) {
|
|
1614
|
-
// obj._id = { $type: "oid", $value: obj.id.$id };
|
|
1615
|
-
// obj.id = obj.id.$id;
|
|
1616
|
-
// }
|
|
1617
|
-
|
|
1618
|
-
// // Appel récursif pour chaque propriété de l'objet
|
|
1619
|
-
// Object.keys(obj).forEach(key => {
|
|
1620
|
-
// if (obj[key] && typeof obj[key] === "object") {
|
|
1621
|
-
// obj[key] = this._normalizeIdRecursively(obj[key]);
|
|
1622
|
-
// }
|
|
1623
|
-
// });
|
|
1624
|
-
|
|
1625
|
-
// return obj;
|
|
1626
|
-
// }
|
|
1627
|
-
|
|
1628
|
-
/**
|
|
1629
|
-
* Normalise récursivement les valeurs booléennes.
|
|
1630
|
-
* Si une valeur est une chaîne "true" ou "false", elle est convertie en booléen.
|
|
1631
|
-
*
|
|
1632
|
-
* @param data - L'objet, le tableau ou la valeur à normaliser.
|
|
1633
|
-
* @returns La donnée normalisée.
|
|
1634
|
-
*/
|
|
1635
|
-
// _normalizeBooleansRecursively(data) {
|
|
1636
|
-
// if (typeof data === "string") {
|
|
1637
|
-
// if (data === "true") return true;
|
|
1638
|
-
// if (data === "false") return false;
|
|
1639
|
-
// return data;
|
|
1640
|
-
// }
|
|
1641
|
-
// if (Array.isArray(data)) {
|
|
1642
|
-
// return data.map(item => this._normalizeBooleansRecursively(item));
|
|
1643
|
-
// }
|
|
1644
|
-
// if (data !== null && typeof data === "object") {
|
|
1645
|
-
// Object.keys(data).forEach(key => {
|
|
1646
|
-
// data[key] = this._normalizeBooleansRecursively(data[key]);
|
|
1647
|
-
// });
|
|
1648
|
-
// }
|
|
1649
|
-
// return data;
|
|
1650
|
-
// }
|
|
1651
|
-
|
|
1652
|
-
/**
|
|
1653
|
-
* Transforme une chaîne "true" ou "false" en booléen.
|
|
1654
|
-
*
|
|
1655
|
-
* @param str - La chaîne à normaliser.
|
|
1656
|
-
* @returns La valeur booléenne ou la chaîne originale.
|
|
1657
|
-
*/
|
|
1658
|
-
private _normalizeString(str: any) {
|
|
1659
|
-
if (str === "true") return true;
|
|
1660
|
-
if (str === "false") return false;
|
|
1661
|
-
return str;
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
/**
|
|
1665
|
-
* Normalise les identifiants dans un objet.
|
|
1666
|
-
* Si l'objet possède une propriété "_id" ou "id" contenant un sous-objet "$id" valide,
|
|
1667
|
-
* il met à jour l'objet en assignant la valeur de l'ID et en formattant _id en EJSON.
|
|
1668
|
-
*
|
|
1669
|
-
* @param obj - L'objet à normaliser.
|
|
1670
|
-
* @returns L'objet avec l'ID normalisé.
|
|
1671
|
-
*/
|
|
1672
|
-
private _normalizeId(obj: any) {
|
|
1673
|
-
if (obj._id && obj._id.$id && /^[0-9a-fA-F]{24}$/.test(obj._id.$id)) {
|
|
1674
|
-
obj.id = obj._id.$id;
|
|
1675
|
-
obj._id = { $type: "oid", $value: obj._id.$id };
|
|
1676
|
-
}
|
|
1677
|
-
if (obj.id && obj.id.$id && /^[0-9a-fA-F]{24}$/.test(obj.id.$id)) {
|
|
1678
|
-
obj._id = { $type: "oid", $value: obj.id.$id };
|
|
1679
|
-
obj.id = obj.id.$id;
|
|
1680
|
-
}
|
|
1681
|
-
return obj;
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* Normalise une valeur de date.
|
|
1686
|
-
* Si la valeur est un objet contenant "sec" (nombre) ou un nombre,
|
|
1687
|
-
* elle est convertie en { $date: valeurEnMs }.
|
|
1688
|
-
*
|
|
1689
|
-
* @param value - La valeur à normaliser.
|
|
1690
|
-
* @returns La valeur normalisée.
|
|
1691
|
-
*/
|
|
1692
|
-
private _normalizeDate(value: any) {
|
|
1693
|
-
if (value && typeof value === "object" && typeof value.sec === "number") {
|
|
1694
|
-
return { $date: value.sec * 1000 };
|
|
1695
|
-
} else if (typeof value === "number") {
|
|
1696
|
-
return { $date: value * 1000 };
|
|
1697
|
-
} else if (typeof value === "string" && value.trim() !== "") {
|
|
1698
|
-
const date = new Date(value);
|
|
1699
|
-
// Vérifier que la date est valide (pas NaN)
|
|
1700
|
-
if (!isNaN(date.getTime())) {
|
|
1701
|
-
return EJSON.toJSONValue(date);
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
return value;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
/**
|
|
1708
|
-
* Normalise une URL d'image.
|
|
1709
|
-
* Si la valeur est une chaîne non vide, retourne l'URL complète via _ensureFullURL.
|
|
1710
|
-
*
|
|
1711
|
-
* @param value - La valeur à normaliser.
|
|
1712
|
-
* @returns La valeur normalisée.
|
|
1713
|
-
*/
|
|
1714
|
-
private _normalizeImage(value: any) {
|
|
1715
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
1716
|
-
return this._ensureFullURL(value);
|
|
1717
|
-
}
|
|
1718
|
-
return value;
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
/**
|
|
1722
|
-
* Normalise les valeurs numériques renvoyées en string par le backend
|
|
1723
|
-
* (cas typique : content-type application/x-www-form-urlencoded qui
|
|
1724
|
-
* sérialise tout en string, et le backend stocke tel quel).
|
|
1725
|
-
*
|
|
1726
|
-
* @param value - La valeur à normaliser.
|
|
1727
|
-
* @returns Number si conversion possible, sinon la valeur d'origine.
|
|
1728
|
-
*/
|
|
1729
|
-
private _normalizeNumber(value: any) {
|
|
1730
|
-
if (typeof value !== "string") return value;
|
|
1731
|
-
const trimmed = value.trim();
|
|
1732
|
-
if (trimmed === "") return value;
|
|
1733
|
-
const n = Number(trimmed);
|
|
1734
|
-
return Number.isFinite(n) ? n : value;
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
/**
|
|
1738
|
-
* Liste des champs d'image à normaliser.
|
|
1739
|
-
*/
|
|
1740
|
-
private _imageFields = [
|
|
1741
|
-
"profilImageUrl",
|
|
1742
|
-
"profilThumbImageUrl",
|
|
1743
|
-
"profilMediumImageUrl",
|
|
1744
|
-
"profilMarkerImageUrl",
|
|
1745
|
-
"profilBannerUrl",
|
|
1746
|
-
"profilRealBannerUrl",
|
|
1747
|
-
"imagePath",
|
|
1748
|
-
"imageThumbPath",
|
|
1749
|
-
"docPath",
|
|
1750
|
-
"imageThumb",
|
|
1751
|
-
"imageMediumPath",
|
|
1752
|
-
"logo",
|
|
1753
|
-
"logoMin",
|
|
1754
|
-
// `image` : chemin relatif renvoyé par Document::getDocumentPath (ex: valeurs
|
|
1755
|
-
// thématiques de COSTUM_FILTER_COFORM_BY_PATH). _normalizeImage ne touche que les
|
|
1756
|
-
// strings non vides → "" et non-strings restent inchangés (pas d'effet de bord).
|
|
1757
|
-
"image"
|
|
1758
|
-
];
|
|
1759
|
-
|
|
1760
|
-
/**
|
|
1761
|
-
* Liste des champs de date à normaliser.
|
|
1762
|
-
*/
|
|
1763
|
-
// champs d'ENTITÉ génériques + champs costum (auto-générés depuis l'artefact via normalize.ts, coercés par NOM
|
|
1764
|
-
// sans dépendre de source.key ; les champs au type divergent entre costums — phone/rate — en sont exclus).
|
|
1765
|
-
private _dateFields = ["modified", "created", "updated", "birthDate", "lastLoginDate", "startDate", "endDate", "date", "issuedOn", ...COSTUM_DATE_FIELDS];
|
|
1766
|
-
|
|
1767
|
-
/**
|
|
1768
|
-
* Liste des champs numériques à normaliser (le backend les renvoie en string
|
|
1769
|
-
* via x-www-form-urlencoded ; sans cette coercion, JSON.stringify diff
|
|
1770
|
-
* produit des faux positifs dans `hasChanges()`).
|
|
1771
|
-
*/
|
|
1772
|
-
private _numberFields = ["credits", "min", "max", "timeSpent", ...COSTUM_NUMBER_FIELDS];
|
|
1773
|
-
|
|
1774
|
-
/**
|
|
1775
|
-
* Normalise récursivement un objet, un tableau ou une valeur simple.
|
|
1776
|
-
* Cette fonction réalise en une seule passe les opérations suivantes :
|
|
1777
|
-
* - Conversion des chaînes "true"/"false" en booléens.
|
|
1778
|
-
* - Normalisation des dates (pour les champs spécifiés).
|
|
1779
|
-
* - Normalisation des images (pour les champs spécifiés).
|
|
1780
|
-
* - Transformation récursive des objets et tableaux.
|
|
1781
|
-
* - Normalisation des identifiants.
|
|
1782
|
-
*
|
|
1783
|
-
* @param data - La donnée à normaliser.
|
|
1784
|
-
* @returns La donnée normalisée.
|
|
1785
|
-
*/
|
|
1786
|
-
private _normalizeRecursively(data: any): any {
|
|
1787
|
-
// Cas de base pour les chaînes
|
|
1788
|
-
if (typeof data === "string") {
|
|
1789
|
-
return this._normalizeString(data);
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
// Traitement des tableaux
|
|
1793
|
-
if (Array.isArray(data)) {
|
|
1794
|
-
return data.map((item: any) => this._normalizeRecursively(item));
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
// Traitement des objets non nuls
|
|
1798
|
-
if (data !== null && typeof data === "object") {
|
|
1799
|
-
// On crée une copie de l'objet pour éviter les effets de bord.
|
|
1800
|
-
const normalizedData: any = {};
|
|
1801
|
-
|
|
1802
|
-
Object.keys(data).forEach(key => {
|
|
1803
|
-
// Appliquer récursivement la normalisation sur la valeur.
|
|
1804
|
-
let value = this._normalizeRecursively(data[key]);
|
|
1805
|
-
|
|
1806
|
-
// Si la clé correspond à un champ de date, on applique la normalisation de date.
|
|
1807
|
-
if (this._dateFields.includes(key)) {
|
|
1808
|
-
value = this._normalizeDate(value);
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
// Si la clé correspond à un champ d'image, on applique la normalisation d'image.
|
|
1812
|
-
if (this._imageFields.includes(key)) {
|
|
1813
|
-
value = this._normalizeImage(value);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
// Si la clé correspond à un champ numérique, on coerce la string en number.
|
|
1817
|
-
if (this._numberFields.includes(key)) {
|
|
1818
|
-
value = this._normalizeNumber(value);
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
// Cas particulier : si la clé est "content" et qu'elle possède une propriété "image".
|
|
1822
|
-
if (key === "content" && value && typeof value === "object" && value.image) {
|
|
1823
|
-
value.image = this._normalizeImage(value.image);
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
normalizedData[key] = value;
|
|
1827
|
-
});
|
|
1828
|
-
|
|
1829
|
-
// Normalisation des identifiants après traitement de l'objet.
|
|
1830
|
-
return this._normalizeId(normalizedData);
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
// Pour les autres types (nombres, booléens déjà, etc.), on renvoie la donnée inchangée.
|
|
1834
|
-
return data;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
private _startBeforeEndValidate = (schema: any, data: any) => {
|
|
1838
|
-
if (!data.startDate || !data.endDate) return true;
|
|
1839
|
-
|
|
1840
|
-
const isValid = new Date(data.startDate) < new Date(data.endDate);
|
|
1841
|
-
if (!isValid) {
|
|
1842
|
-
// @ts-expect-error - on sait que ValidateFunction expose .errors au runtime
|
|
1843
|
-
this._startBeforeEndValidate.errors = [
|
|
1844
|
-
{
|
|
1845
|
-
instancePath: "/startDate", // ou "." si tu veux le chemin actuel
|
|
1846
|
-
schemaPath: "#/startBeforeEnd",
|
|
1847
|
-
keyword: "startBeforeEnd",
|
|
1848
|
-
params: {},
|
|
1849
|
-
message: "startDate must be before endDate"
|
|
1850
|
-
}
|
|
1851
|
-
];
|
|
1852
|
-
}
|
|
1853
|
-
return isValid;
|
|
1854
|
-
};
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
/**
|
|
1858
|
-
* Récupère le schéma de requête pour un endpoint donné.
|
|
1859
|
-
* @param constant - Le nom de l'endpoint.
|
|
1860
|
-
* @returns Le schéma de requête ou null si non trouvé.
|
|
1861
|
-
*/
|
|
1862
|
-
getRequestSchema(constant: string) {
|
|
1863
|
-
const endpoint = this._endpoints.find(e => e.constant === constant);
|
|
1864
|
-
return endpoint?.request || null;
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
/**
|
|
1868
|
-
* Récupère le schéma de chemin pour un endpoint donné.
|
|
1869
|
-
* @param constant - Le nom de l'endpoint.
|
|
1870
|
-
* @returns Le schéma de chemin ou null si non trouvé.
|
|
1871
|
-
*/
|
|
1872
|
-
getPathSchema(constant: string) {
|
|
1873
|
-
return this._endpoints.find(e => e.constant === constant)?.pathParams || null;
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
/**
|
|
1877
|
-
* Permet d'écouter facilement un ensemble d'événements importants émis par l'ApiClient.
|
|
1878
|
-
*
|
|
1879
|
-
* @param handlers - Un objet avec des fonctions à appeler selon l'événement.
|
|
1880
|
-
* @param [handlers.retryAttempt] - Lors d'une tentative de retry axios.
|
|
1881
|
-
* @param [handlers.queuedOffline] - Lorsqu'une requête est mise en file.
|
|
1882
|
-
* @param [handlers.circuitBreakerOpen] - Quand le breaker s'ouvre.
|
|
1883
|
-
* @param [handlers.circuitBreakerReset] - Quand le breaker se referme.
|
|
1884
|
-
* @param [handlers.refreshSuccess] - Quand le token est rafraîchi.
|
|
1885
|
-
* @param [handlers.refreshFailed] - Quand le refresh échoue.
|
|
1886
|
-
* @param [handlers.sessionReset] - Quand la session est réinitialisée.
|
|
1887
|
-
* @param [handlers.validationError] - Quand une validation échoue.
|
|
1888
|
-
* @param [handlers.offlineModeChanged] - Quand le mode offline change.
|
|
1889
|
-
* @param [handlers.userLoggedIn] - Quand un utilisateur se connecte.
|
|
1890
|
-
*/
|
|
1891
|
-
onEvent(handlers: Record<string, (...args: any[]) => void> = {}) {
|
|
1892
|
-
const availableEvents = [
|
|
1893
|
-
"retryAttempt",
|
|
1894
|
-
"queuedOffline",
|
|
1895
|
-
"circuitBreakerOpen",
|
|
1896
|
-
"circuitBreakerReset",
|
|
1897
|
-
"refreshSuccess",
|
|
1898
|
-
"refreshFailed",
|
|
1899
|
-
"sessionReset",
|
|
1900
|
-
"validationError",
|
|
1901
|
-
"offlineModeChanged",
|
|
1902
|
-
"userLoggedIn"
|
|
1903
|
-
];
|
|
1904
|
-
|
|
1905
|
-
for (const eventName of availableEvents) {
|
|
1906
|
-
if (typeof handlers[eventName] === "function") {
|
|
1907
|
-
this.on(eventName, handlers[eventName] as (...args: any[]) => void);
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
/**
|
|
1913
|
-
* Retourne la liste des noms d'événements personnalisés déclarés dans les endpoints.
|
|
1914
|
-
* Utile pour introspection ou documentation.
|
|
1915
|
-
*
|
|
1916
|
-
* @returns Liste des événements émis dynamiquement via postActions.
|
|
1917
|
-
*/
|
|
1918
|
-
getDeclaredEvents() {
|
|
1919
|
-
const events = new Set();
|
|
1920
|
-
for (const endpoint of this._endpoints) {
|
|
1921
|
-
if (Array.isArray(endpoint.postActions)) {
|
|
1922
|
-
for (const action of endpoint.postActions) {
|
|
1923
|
-
if (action.type === "emitEvent" && action.event) {
|
|
1924
|
-
events.add(action.event);
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
return Array.from(events);
|
|
1930
|
-
}
|
|
1931
|
-
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
/**
|
|
1935
|
-
* @event ApiClient#retryAttempt
|
|
1936
|
-
* @type {Object}
|
|
1937
|
-
* @property {number} retryCount - Le numéro de tentative.
|
|
1938
|
-
* @property {string} url - L'URL de la requête ayant échoué.
|
|
1939
|
-
*/
|
|
1940
|
-
|
|
1941
|
-
/**
|
|
1942
|
-
* @event ApiClient#queuedOffline
|
|
1943
|
-
* @type {Object}
|
|
1944
|
-
* @property {string} constant - Le nom de l'endpoint.
|
|
1945
|
-
* @property {Object} data - Les données envoyées.
|
|
1946
|
-
* @property {"offlineMode"|"circuitBreaker"} reason - La raison de la mise en file.
|
|
1947
|
-
*/
|
|
1948
|
-
|
|
1949
|
-
/**
|
|
1950
|
-
* @event ApiClient#circuitBreakerOpen
|
|
1951
|
-
* @type {Object}
|
|
1952
|
-
* @property {number} timestamp - Date (ms) à laquelle le breaker s'est ouvert.
|
|
1953
|
-
*/
|
|
1954
|
-
|
|
1955
|
-
/**
|
|
1956
|
-
* @event ApiClient#circuitBreakerReset
|
|
1957
|
-
* @type {void}
|
|
1958
|
-
*/
|
|
1959
|
-
|
|
1960
|
-
/**
|
|
1961
|
-
* @event ApiClient#refreshSuccess
|
|
1962
|
-
* @type {Object}
|
|
1963
|
-
* @property {string} token - Le nouveau token d'accès.
|
|
1964
|
-
* @property {string} [refreshToken] - Le nouveau refreshToken (optionnel).
|
|
1965
|
-
*/
|
|
1966
|
-
|
|
1967
|
-
/**
|
|
1968
|
-
* @event ApiClient#refreshFailed
|
|
1969
|
-
* @type {Object}
|
|
1970
|
-
* @property {string} error - Le message d’erreur de refresh.
|
|
1971
|
-
*/
|
|
1972
|
-
|
|
1973
|
-
/**
|
|
1974
|
-
* @event ApiClient#sessionReset
|
|
1975
|
-
* @type {void}
|
|
1976
|
-
*/
|
|
1977
|
-
|
|
1978
|
-
/**
|
|
1979
|
-
* @event ApiClient#validationError
|
|
1980
|
-
* @type {Object}
|
|
1981
|
-
* @property {"pathParams"|"request"|"response"} stage - Étape de validation échouée.
|
|
1982
|
-
* @property {Array<Object>} errors - Erreurs AJV brutes.
|
|
1983
|
-
*/
|
|
1984
|
-
|
|
1985
|
-
/**
|
|
1986
|
-
* @event ApiClient#offlineModeChanged
|
|
1987
|
-
* @type {boolean} - true si le client est offline, false sinon.
|
|
1988
|
-
*/
|
|
1989
|
-
|
|
1990
|
-
/**
|
|
1991
|
-
* @event ApiClient#userLoggedIn
|
|
1992
|
-
* @param user - Les données utilisateur extraites de la réponse.
|
|
1993
|
-
*/
|
|
1994
|
-
|