@communecter/cocolight-api-client 1.0.0
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/README.md +194 -0
- package/dist/cocolight-api-client.browser.js +7 -0
- package/dist/cocolight-api-client.cjs +1 -0
- package/dist/cocolight-api-client.cjs.js +1 -0
- package/dist/cocolight-api-client.mjs.js +1 -0
- package/package.json +79 -0
- package/src/ApiClient.js +1083 -0
- package/src/EJSONType.js +53 -0
- package/src/endpoints.module.js +4026 -0
- package/src/error.js +20 -0
package/src/ApiClient.js
ADDED
|
@@ -0,0 +1,1083 @@
|
|
|
1
|
+
import { EventEmitter } from "events";
|
|
2
|
+
|
|
3
|
+
import Ajv from "ajv";
|
|
4
|
+
import addFormats from "ajv-formats";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import axiosRetry from "axios-retry";
|
|
7
|
+
import EJSON from "ejson";
|
|
8
|
+
import pino from "pino";
|
|
9
|
+
|
|
10
|
+
import MongoID from "./EJSONType.js";
|
|
11
|
+
import endpointsJson from "./endpoints.module.js";
|
|
12
|
+
import { ApiClientError, CircuitBreakerError } from "./error.js";
|
|
13
|
+
|
|
14
|
+
EJSON.addType("oid", value => {
|
|
15
|
+
return new MongoID.ObjectID(value);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export default class ApiClient extends EventEmitter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} options
|
|
21
|
+
* @param {string} options.baseURL
|
|
22
|
+
* @param {string} [options.accessToken]
|
|
23
|
+
* @param {string} [options.refreshToken]
|
|
24
|
+
* @param {string} [options.refreshUrl=/api/cocolight/refreshtoken]
|
|
25
|
+
* @param {Array} [options.endpoints=endpointsJson.endpoints]
|
|
26
|
+
* @param {number} [options.timeout=30000]
|
|
27
|
+
* @param {boolean} [options.debug=false]
|
|
28
|
+
* @param {number} [options.maxRetries=0] - Nombre de tentatives auto (axios-retry)
|
|
29
|
+
* @param {number} [options.circuitBreakerThreshold=5] - Nb d'erreurs avant de bloquer
|
|
30
|
+
* @param {number} [options.circuitBreakerResetTime=60000] - Ms avant de reset le breaker
|
|
31
|
+
* @param {boolean} [options.fromJSONValue=true] - Si true, les données sont transformées en EJSON
|
|
32
|
+
*/
|
|
33
|
+
constructor({
|
|
34
|
+
baseURL,
|
|
35
|
+
accessToken,
|
|
36
|
+
refreshToken,
|
|
37
|
+
refreshUrl = "/api/cocolight/refreshtoken",
|
|
38
|
+
endpoints = endpointsJson.endpoints,
|
|
39
|
+
timeout = 30000,
|
|
40
|
+
debug = false,
|
|
41
|
+
maxRetries = 0,
|
|
42
|
+
circuitBreakerThreshold = 5,
|
|
43
|
+
circuitBreakerResetTime = 60000,
|
|
44
|
+
fromJSONValue = true
|
|
45
|
+
} = {}) {
|
|
46
|
+
super(); // EventEmitter
|
|
47
|
+
|
|
48
|
+
if (!baseURL) {
|
|
49
|
+
throw new ApiClientError("Le paramètre \"baseURL\" est obligatoire.", 500);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this._baseURL = baseURL;
|
|
53
|
+
this._refreshToken = refreshToken;
|
|
54
|
+
this._refreshUrl = refreshUrl;
|
|
55
|
+
this._endpoints = endpoints;
|
|
56
|
+
this._debug = debug;
|
|
57
|
+
let _userId = null;
|
|
58
|
+
|
|
59
|
+
// Active la transformation des données en EJSON globalement
|
|
60
|
+
this._fromJSONValue = fromJSONValue;
|
|
61
|
+
|
|
62
|
+
Object.defineProperty(this, "userId", {
|
|
63
|
+
get: () => _userId,
|
|
64
|
+
set: () => {
|
|
65
|
+
throw new Error("Modification directe de userId non autorisée.");
|
|
66
|
+
},
|
|
67
|
+
enumerable: true
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this._setUserId = (id) => {
|
|
71
|
+
_userId = id;
|
|
72
|
+
this._logger.debug(`[ApiClient] userId set: ${id}`);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// AJV
|
|
76
|
+
this._ajv = new Ajv({ strict: false, useDefaults: true });
|
|
77
|
+
addFormats(this._ajv);
|
|
78
|
+
|
|
79
|
+
// Pino logger
|
|
80
|
+
// (Ici en mode pretty-print sur la console, tu peux configurer comme tu veux)
|
|
81
|
+
this._logger = pino({
|
|
82
|
+
transport: {
|
|
83
|
+
target: "pino-pretty",
|
|
84
|
+
options: { colorize: true }
|
|
85
|
+
},
|
|
86
|
+
level: debug ? "debug" : "info"
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Création instance Axios
|
|
90
|
+
this._client = axios.create({
|
|
91
|
+
baseURL,
|
|
92
|
+
timeout
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// axios-retry : pour retenter en cas de soucis réseau ou code 5xx
|
|
96
|
+
if (maxRetries > 0) {
|
|
97
|
+
axiosRetry(this._client, {
|
|
98
|
+
retries: maxRetries,
|
|
99
|
+
retryDelay: axiosRetry.exponentialDelay,
|
|
100
|
+
retryCondition: (error) => {
|
|
101
|
+
// Retry sur erreurs 5xx ou erreurs réseau
|
|
102
|
+
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
this._logger.info(`[ApiClient] Retry activé : ${maxRetries} max`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Circuit breaker (simplifié)
|
|
109
|
+
this._breakerThreshold = circuitBreakerThreshold;
|
|
110
|
+
this._breakerResetTime = circuitBreakerResetTime;
|
|
111
|
+
this._breakerErrorCount = 0;
|
|
112
|
+
this._breakerOpen = false;
|
|
113
|
+
this._lastBreakerOpenTime = null;
|
|
114
|
+
|
|
115
|
+
// Applique un token initial
|
|
116
|
+
if (accessToken) {
|
|
117
|
+
this.setToken(accessToken);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Intercepteur 401 -> refresh
|
|
121
|
+
this._client.interceptors.response.use(
|
|
122
|
+
(response) => response,
|
|
123
|
+
async (error) => {
|
|
124
|
+
if (error.response && error.response.status === 401) {
|
|
125
|
+
if (this._refreshToken) {
|
|
126
|
+
try {
|
|
127
|
+
const refreshed = await this._refreshAccessToken();
|
|
128
|
+
if (refreshed) {
|
|
129
|
+
return this._client.request(error.config);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
throw new ApiClientError(err.message, 401, err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Sets the access token for the API client and updates the authorization header.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} token - The access token to be set.
|
|
145
|
+
*/
|
|
146
|
+
setToken(token) {
|
|
147
|
+
this._accessToken = token;
|
|
148
|
+
this._client.defaults.headers.common["Authorization"] = "Bearer " + token;
|
|
149
|
+
this._logger.debug(`[ApiClient] setToken: ${token}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Retrieves the current access token.
|
|
154
|
+
*
|
|
155
|
+
* @returns {string} The access token.
|
|
156
|
+
*/
|
|
157
|
+
getToken() {
|
|
158
|
+
return this._accessToken;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Sets the refresh token for the API client.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} rt - The refresh token to be set.
|
|
165
|
+
*/
|
|
166
|
+
setRefreshToken(rt) {
|
|
167
|
+
this._refreshToken = rt;
|
|
168
|
+
this._logger.debug(`[ApiClient] setRefreshToken: ${rt}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Retrieves the current refresh token.
|
|
173
|
+
*
|
|
174
|
+
* @returns {string} The refresh token.
|
|
175
|
+
*/
|
|
176
|
+
getRefreshToken() {
|
|
177
|
+
return this._refreshToken;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Méthode simplifiée de refresh (en JSON).
|
|
182
|
+
* Emet un event refreshSuccess si ça marche
|
|
183
|
+
*/
|
|
184
|
+
async _refreshAccessToken() {
|
|
185
|
+
if (!this._refreshToken) return false;
|
|
186
|
+
try {
|
|
187
|
+
const response = await this._client.post(
|
|
188
|
+
this._refreshUrl,
|
|
189
|
+
{ refreshToken: this._refreshToken },
|
|
190
|
+
{ headers: { "Content-Type": "application/json" } }
|
|
191
|
+
);
|
|
192
|
+
if (response.data && response.data.accessToken) {
|
|
193
|
+
this.setToken(response.data.accessToken);
|
|
194
|
+
if (response.data.refreshToken) {
|
|
195
|
+
this.setRefreshToken(response.data.refreshToken);
|
|
196
|
+
}
|
|
197
|
+
this.emit("refreshSuccess", response.data);
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
} catch (err) {
|
|
202
|
+
this._logger.error(`[ApiClient] Refresh Error : ${err.message}`);
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* checkCircuitBreaker : vérifie si on peut appeler l'API ou non
|
|
209
|
+
* si le breaker est \"open\", on regarde si on peut \"reset\"
|
|
210
|
+
*/
|
|
211
|
+
_checkCircuitBreaker() {
|
|
212
|
+
if (!this._breakerOpen) return true;
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
if (now - this._lastBreakerOpenTime > this._breakerResetTime) {
|
|
215
|
+
// On reset
|
|
216
|
+
this._breakerOpen = false;
|
|
217
|
+
this._breakerErrorCount = 0;
|
|
218
|
+
this._logger.warn("[ApiClient] Circuit breaker réinitialisé");
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* updateCircuitBreaker : incremente le compteur d'erreurs
|
|
226
|
+
* si on dépasse le threshold, on \"open\" le breaker.
|
|
227
|
+
*/
|
|
228
|
+
_updateCircuitBreakerError() {
|
|
229
|
+
this._breakerErrorCount += 1;
|
|
230
|
+
this._logger.warn(`[ApiClient] Erreur #${this._breakerErrorCount} sur ${this._breakerThreshold}`);
|
|
231
|
+
if (this._breakerErrorCount >= this._breakerThreshold) {
|
|
232
|
+
this._breakerOpen = true;
|
|
233
|
+
this._lastBreakerOpenTime = Date.now();
|
|
234
|
+
this._logger.error("[ApiClient] Circuit breaker ACTIVÉ - L'API est considérée indisponible");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* resetCircuitBreaker : en cas de succès on reset le compteur
|
|
240
|
+
*/
|
|
241
|
+
_resetCircuitBreakerSuccess() {
|
|
242
|
+
this._breakerErrorCount = 0;
|
|
243
|
+
if (this._breakerOpen) {
|
|
244
|
+
this._breakerOpen = false;
|
|
245
|
+
this._logger.warn("[ApiClient] Circuit breaker refermé suite à un succès");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static stripNullsInPlace(obj) {
|
|
250
|
+
// 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.
|
|
251
|
+
if (obj && typeof obj === "object" && obj.value && typeof obj.value.pipe === "function") {
|
|
252
|
+
return obj;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(obj)) {
|
|
256
|
+
for (let i = obj.length - 1; i >= 0; i--) {
|
|
257
|
+
if (obj[i] === null || obj[i] === undefined) {
|
|
258
|
+
obj.splice(i, 1);
|
|
259
|
+
} else if (typeof obj[i] === "object") {
|
|
260
|
+
ApiClient.stripNullsInPlace(obj[i]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else if (obj && typeof obj === "object") {
|
|
264
|
+
for (const key in obj) {
|
|
265
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
266
|
+
if (obj[key] === null || obj[key] === undefined) {
|
|
267
|
+
delete obj[key];
|
|
268
|
+
} else if (typeof obj[key] === "object") {
|
|
269
|
+
ApiClient.stripNullsInPlace(obj[key]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return obj;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_resolveSpecialValuesInPlace(obj) {
|
|
278
|
+
// Si l'objet ressemble à un fichier uploadé (détecte la présence d'un stream dans la propriété value), ne rien modifier.
|
|
279
|
+
if (obj && typeof obj === "object" && obj.value && typeof obj.value.pipe === "function") {
|
|
280
|
+
return obj;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const aliasMap = {
|
|
284
|
+
userId: () => this.userId,
|
|
285
|
+
accessToken: () => this._accessToken,
|
|
286
|
+
refreshToken: () => this._refreshToken,
|
|
287
|
+
baseURL: () => this._baseURL,
|
|
288
|
+
// vous pouvez ajouter d'autres alias ici
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (typeof obj === "string" && obj.startsWith("@")) {
|
|
292
|
+
const varName = obj.slice(1);
|
|
293
|
+
return typeof aliasMap[varName] === "function" ? aliasMap[varName]() : obj;
|
|
294
|
+
} else if (Array.isArray(obj)) {
|
|
295
|
+
for (let i = 0; i < obj.length; i++) {
|
|
296
|
+
obj[i] = this._resolveSpecialValuesInPlace(obj[i]);
|
|
297
|
+
}
|
|
298
|
+
} else if (obj && typeof obj === "object") {
|
|
299
|
+
for (const key in obj) {
|
|
300
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
301
|
+
obj[key] = this._resolveSpecialValuesInPlace(obj[key]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return obj;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Calls an API endpoint with the specified parameters.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} constant - The constant representing the endpoint to call.
|
|
313
|
+
* @param {Object} [data={}] - The data to send with the request.
|
|
314
|
+
* @param {boolean|function} [transformResponseData=true] - Whether to transform the response data or a function to transform it.
|
|
315
|
+
* @param {boolean} [validateResponseSchema=true] - Whether to validate the response schema.
|
|
316
|
+
* @returns {Promise<Object>} The response from the API.
|
|
317
|
+
* @throws {CircuitBreakerError} If the circuit breaker is activated.
|
|
318
|
+
* @throws {ApiClientError} If the endpoint is not found, token is required but not provided, or validation fails.
|
|
319
|
+
*/
|
|
320
|
+
async callEndpoint(constant, data = {}, transformResponseData = true, validateResponseSchema = true) {
|
|
321
|
+
if (!this._checkCircuitBreaker()) {
|
|
322
|
+
throw new CircuitBreakerError("Le circuit breaker est activé, impossible d'appeler l'API");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const endpoint = this._endpoints.find((ep) => ep.constant === constant);
|
|
326
|
+
if (!endpoint) {
|
|
327
|
+
throw new ApiClientError(`Endpoint introuvable : ${constant}`, 404);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const {
|
|
331
|
+
path,
|
|
332
|
+
method,
|
|
333
|
+
contentType,
|
|
334
|
+
auth,
|
|
335
|
+
pathParams: pathSchema,
|
|
336
|
+
request: requestSchema,
|
|
337
|
+
responses
|
|
338
|
+
} = endpoint;
|
|
339
|
+
|
|
340
|
+
const lowerMethod = (method || "GET").toLowerCase();
|
|
341
|
+
const realContentType = contentType || "application/json";
|
|
342
|
+
const headers = { "Content-Type": realContentType };
|
|
343
|
+
|
|
344
|
+
// Auth headers
|
|
345
|
+
if (this._accessToken) {
|
|
346
|
+
if (auth === "bearer") {
|
|
347
|
+
headers["Authorization"] = `Bearer ${this._accessToken}`;
|
|
348
|
+
} else if (!auth || auth === "none") {
|
|
349
|
+
if (!headers["Authorization"]) {
|
|
350
|
+
headers["Authorization"] = `Bearer ${this._accessToken}`;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} else if (auth === "bearer") {
|
|
354
|
+
throw new ApiClientError(`Token requis pour l'endpoint sécurisé : ${constant}`, 401);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// === 1. PathParams ===
|
|
358
|
+
let resolvedPath = path;
|
|
359
|
+
if (pathSchema) {
|
|
360
|
+
const pathParams = data.pathParams || {};
|
|
361
|
+
const validatePathParams = this._ajv.compile(pathSchema);
|
|
362
|
+
const valid = validatePathParams(pathParams);
|
|
363
|
+
|
|
364
|
+
if (!valid) {
|
|
365
|
+
this.emit("validationError", { stage: "pathParams", errors: validatePathParams.errors });
|
|
366
|
+
throw new ApiClientError("Path parameter validation failed.", 400, validatePathParams.errors);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const resolvedParams = this._resolveSpecialValuesInPlace(pathParams);
|
|
370
|
+
|
|
371
|
+
resolvedPath = resolvedPath.replace(/\{(\w+)\}/g, (_, key) => {
|
|
372
|
+
const val = resolvedParams[key];
|
|
373
|
+
if (val !== undefined) return encodeURIComponent(val);
|
|
374
|
+
throw new ApiClientError(`Path param manquant ou non résolu : {${key}}`, 400);
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// === 2. Validation données (request schema) ===
|
|
379
|
+
if (requestSchema) {
|
|
380
|
+
const dataForValidation = { ...data };
|
|
381
|
+
delete dataForValidation.pathParams;
|
|
382
|
+
const cleanedData = ApiClient.stripNullsInPlace(dataForValidation);
|
|
383
|
+
|
|
384
|
+
const validateRequest = this._ajv.compile(requestSchema);
|
|
385
|
+
const valid = validateRequest(cleanedData);
|
|
386
|
+
if (!valid) {
|
|
387
|
+
this.emit("validationError", { stage: "request", errors: validateRequest.errors });
|
|
388
|
+
throw new ApiClientError("Request validation failed.", 400, validateRequest.errors);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
data = this._resolveSpecialValuesInPlace(cleanedData);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// === 3. Payload ===
|
|
395
|
+
let payload;
|
|
396
|
+
if (realContentType === "application/json" || realContentType === "multipart/form-data") {
|
|
397
|
+
payload = data;
|
|
398
|
+
} else if (realContentType === "application/x-www-form-urlencoded") {
|
|
399
|
+
payload = ApiClient.toURLSearchParams(data);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
this._logger.debug(`[ApiClient] callEndpoint: ${constant} -> ${resolvedPath}, method=${lowerMethod}`);
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const response = await this._client.request({
|
|
406
|
+
url: resolvedPath,
|
|
407
|
+
method: lowerMethod,
|
|
408
|
+
headers,
|
|
409
|
+
[lowerMethod === "get" ? "params" : "data"]: payload
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (validateResponseSchema) {
|
|
413
|
+
const status = response.status.toString();
|
|
414
|
+
const schema = responses?.[status];
|
|
415
|
+
if (schema) {
|
|
416
|
+
const validateResponse = this._ajv.compile(schema);
|
|
417
|
+
const valid = validateResponse(response.data);
|
|
418
|
+
if (!valid) {
|
|
419
|
+
this.emit("validationError", { stage: "response", errors: validateResponse.errors });
|
|
420
|
+
throw new ApiClientError("Response validation failed.", status, validateResponse.errors);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this._resetCircuitBreakerSuccess();
|
|
426
|
+
|
|
427
|
+
if (typeof transformResponseData === "function") {
|
|
428
|
+
response.data = transformResponseData(response.data);
|
|
429
|
+
} else if (transformResponseData === true) {
|
|
430
|
+
response.data = this._transformData(response.data);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// postActions éventuelles
|
|
434
|
+
if (Array.isArray(endpoint.postActions)) {
|
|
435
|
+
endpoint.postActions.forEach(action => {
|
|
436
|
+
const value = action.path ? this._getValueByPath(response.data, action.path) : null;
|
|
437
|
+
|
|
438
|
+
switch (action.type) {
|
|
439
|
+
case "setToken":
|
|
440
|
+
this.setToken(value);
|
|
441
|
+
break;
|
|
442
|
+
|
|
443
|
+
case "setRefreshToken":
|
|
444
|
+
this.setRefreshToken(value);
|
|
445
|
+
break;
|
|
446
|
+
|
|
447
|
+
case "setUserId":
|
|
448
|
+
this._setUserId(value);
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
case "resetSession":
|
|
452
|
+
this.resetSession();
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case "emitEvent":
|
|
456
|
+
if (action.event) {
|
|
457
|
+
this.emit(action.event, value);
|
|
458
|
+
this._logger.debug(`[ApiClient] Event émis : ${action.event}`, value);
|
|
459
|
+
}
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case "callMethod":
|
|
463
|
+
if (typeof this[action.method] === "function") {
|
|
464
|
+
this[action.method]();
|
|
465
|
+
this._logger.debug(`[ApiClient] Méthode appelée : ${action.method}`);
|
|
466
|
+
} else {
|
|
467
|
+
this._logger.warn(`[ApiClient] Méthode inconnue : ${action.method}`);
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
|
|
471
|
+
default:
|
|
472
|
+
this._logger.warn(`[ApiClient] Action inconnue : ${action.type}`);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
return response;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
this._updateCircuitBreakerError();
|
|
481
|
+
throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Réinitialise complètement la session de l'utilisateur :
|
|
487
|
+
* - Token d'accès
|
|
488
|
+
* - Token de rafraîchissement
|
|
489
|
+
* - userId
|
|
490
|
+
* - En-têtes Axios
|
|
491
|
+
*/
|
|
492
|
+
resetSession() {
|
|
493
|
+
this.setToken(null);
|
|
494
|
+
this.setRefreshToken(null);
|
|
495
|
+
this._setUserId(null);
|
|
496
|
+
|
|
497
|
+
// Suppression des en-têtes
|
|
498
|
+
delete this._client.defaults.headers.common["Authorization"];
|
|
499
|
+
|
|
500
|
+
this._logger.info("[ApiClient] Session utilisateur réinitialisée.");
|
|
501
|
+
this.emit("sessionReset");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Retrieves the value from an object based on a dot-separated path.
|
|
506
|
+
*
|
|
507
|
+
* @param {Object} obj - The object from which to retrieve the value.
|
|
508
|
+
* @param {string} path - The dot-separated path string indicating the value to retrieve.
|
|
509
|
+
* @returns {*} - The value found at the specified path, or undefined if the path is invalid.
|
|
510
|
+
*/
|
|
511
|
+
_getValueByPath(obj, path) {
|
|
512
|
+
if (!path) return undefined;
|
|
513
|
+
const keys = path.split(".");
|
|
514
|
+
return keys.reduce((acc, key) => acc && acc[key], obj);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Conversions
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Converts an object to URL search parameters.
|
|
521
|
+
*
|
|
522
|
+
* @param {Object} obj - The object to be converted to URL search parameters.
|
|
523
|
+
* @param {Object} [options={}] - Optional settings for the conversion.
|
|
524
|
+
* @returns {URLSearchParams} The URL search parameters generated from the object.
|
|
525
|
+
*/
|
|
526
|
+
static toURLSearchParams(obj, options = {}) {
|
|
527
|
+
return this._buildParams(obj, new URLSearchParams(), options);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Builds parameters for an API request from a given object.
|
|
532
|
+
*
|
|
533
|
+
* @param {Object} obj - The object to be converted into parameters.
|
|
534
|
+
* @param {FormData|URLSearchParams} paramsInstance - The instance to which the parameters will be appended.
|
|
535
|
+
* @param {Object} [options={}] - Optional settings.
|
|
536
|
+
* @param {boolean} [options.dots=false] - Whether to use dots in the parameter keys.
|
|
537
|
+
* @param {boolean} [options.indexes=false] - Whether to include array indexes in the parameter keys.
|
|
538
|
+
* @param {boolean} [options.metaTokens=true] - Whether to include meta tokens in the parameter keys.
|
|
539
|
+
* @throws {TypeError} If the provided obj is not an object or is null.
|
|
540
|
+
* @returns {FormData|URLSearchParams} The instance with the appended parameters.
|
|
541
|
+
*/
|
|
542
|
+
static _buildParams(obj, paramsInstance, options = {}) {
|
|
543
|
+
if (typeof obj !== "object" || obj === null) {
|
|
544
|
+
throw new TypeError("La donnée doit être un objet non nul.");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const { dots = false, indexes = false, metaTokens = true } = options;
|
|
548
|
+
const stack = [];
|
|
549
|
+
|
|
550
|
+
function isVisitable(thing) {
|
|
551
|
+
return Object.prototype.toString.call(thing) === "[object Object]" || Array.isArray(thing);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function removeBrackets(key) {
|
|
555
|
+
return key.endsWith("[]") ? key.slice(0, -2) : key;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function renderKey(path, key, useDots = false) {
|
|
559
|
+
if (!path) return key;
|
|
560
|
+
return path.concat(key).map((token, i) => {
|
|
561
|
+
token = removeBrackets(token);
|
|
562
|
+
return !useDots && i ? `[${token}]` : token;
|
|
563
|
+
}).join(useDots ? "." : "");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function convertValue(value) {
|
|
567
|
+
if (value === null || value === undefined) return "";
|
|
568
|
+
if (value instanceof Date) return value.toISOString();
|
|
569
|
+
if (
|
|
570
|
+
value instanceof Blob ||
|
|
571
|
+
value instanceof File ||
|
|
572
|
+
value instanceof ArrayBuffer ||
|
|
573
|
+
ArrayBuffer.isView(value)
|
|
574
|
+
) {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function defaultVisitor(value, key, path) {
|
|
581
|
+
if (value && typeof value === "object") {
|
|
582
|
+
if (key.endsWith("{}")) {
|
|
583
|
+
key = metaTokens ? key : key.slice(0, -2);
|
|
584
|
+
paramsInstance.append(renderKey(path, key, dots), JSON.stringify(value));
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if ((Array.isArray(value) && !value.some(isVisitable)) || key.endsWith("[]")) {
|
|
589
|
+
key = removeBrackets(key);
|
|
590
|
+
value.forEach((el, index) => {
|
|
591
|
+
if (el !== undefined && el !== null) {
|
|
592
|
+
// const fieldKey = indexes ? renderKey([key], index, dots) : `${key}[]`;
|
|
593
|
+
const fieldKey = indexes
|
|
594
|
+
? renderKey(path.concat(key), index, dots)
|
|
595
|
+
: `${renderKey(path, key, dots)}[]`;
|
|
596
|
+
paramsInstance.append(fieldKey, convertValue(el));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (isVisitable(value)) return true;
|
|
604
|
+
paramsInstance.append(renderKey(path, key, dots), convertValue(value));
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function build(value, path = []) {
|
|
609
|
+
if (value === undefined || value === null || stack.includes(value)) return;
|
|
610
|
+
stack.push(value);
|
|
611
|
+
|
|
612
|
+
Object.entries(value).forEach(([k, el]) => {
|
|
613
|
+
if (el !== undefined && defaultVisitor(el, k.trim(), path)) {
|
|
614
|
+
build(el, path.concat(removeBrackets(k)));
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
stack.pop();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
build(obj);
|
|
622
|
+
return paramsInstance;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Transforms the given data based on its structure.
|
|
628
|
+
*
|
|
629
|
+
* This method normalizes various structures of data into a consistent format.
|
|
630
|
+
* It handles different types of data including objects, arrays, and specific
|
|
631
|
+
* nested structures like `resultGoods`, `resultErrors`, `results`, `news`,
|
|
632
|
+
* `notif`, `citoyens`, `organizations`, `cities`, `newComment`, `map`, and `object`.
|
|
633
|
+
*
|
|
634
|
+
* @param {Object|Array} data - The data to be transformed.
|
|
635
|
+
* @returns {Object|Array} - The transformed data.
|
|
636
|
+
*
|
|
637
|
+
* @private
|
|
638
|
+
*/
|
|
639
|
+
_transformData(data) {
|
|
640
|
+
if (data && typeof data === "object") {
|
|
641
|
+
if (data.resultGoods?.msg) {
|
|
642
|
+
data = this._normalizeJsonData({ msg: data.resultGoods.msg, ...data });
|
|
643
|
+
} else if (data.resultErrors?.msg) {
|
|
644
|
+
data = this._normalizeJsonData({ msg: data.resultErrors.msg, ...data });
|
|
645
|
+
} else if (data.results && typeof data.results === "object" && !Array.isArray(data.results)) {
|
|
646
|
+
data.results = Object.keys(data.results).map((key) => {
|
|
647
|
+
return this._normalizeJsonData({ id: key, ...data.results[key] });
|
|
648
|
+
});
|
|
649
|
+
} else if (Array.isArray(data.results)) {
|
|
650
|
+
data.results = data.results.map((item) => this._normalizeJsonData(item));
|
|
651
|
+
} else if (data.news && Array.isArray(data.news) && data.news.length === 0) {
|
|
652
|
+
data = data.news;
|
|
653
|
+
} else if (data.news && typeof data.news === "object" && !Array.isArray(data.news)) {
|
|
654
|
+
data = Object.keys(data.news).map((key) => {
|
|
655
|
+
return this._normalizeJsonData({ id: key, ...data.news[key] });
|
|
656
|
+
});
|
|
657
|
+
} else if (data.notif && typeof data.notif === "object" && !Array.isArray(data.notif)) {
|
|
658
|
+
data.notif = Object.keys(data.notif).map((key) => {
|
|
659
|
+
return this._normalizeJsonData({ id: key, ...data.notif[key] });
|
|
660
|
+
});
|
|
661
|
+
} else if (
|
|
662
|
+
data.citoyens && typeof data.citoyens === "object" && !Array.isArray(data.citoyens) &&
|
|
663
|
+
data.organizations && typeof data.organizations === "object" && !Array.isArray(data.organizations)
|
|
664
|
+
) {
|
|
665
|
+
data = [
|
|
666
|
+
...Object.keys(data.citoyens).map(key => this._normalizeJsonData({ id: key, ...data.citoyens[key] })),
|
|
667
|
+
...Object.keys(data.organizations).map(key => this._normalizeJsonData({ id: key, ...data.organizations[key] }))
|
|
668
|
+
];
|
|
669
|
+
} else if (
|
|
670
|
+
data.citoyens && typeof data.citoyens === "object" && !Array.isArray(data.citoyens)
|
|
671
|
+
) {
|
|
672
|
+
data = Object.keys(data.citoyens).map(key => this._normalizeJsonData({ id: key, ...data.citoyens[key] }));
|
|
673
|
+
} else if (data.cities && typeof data.cities === "object" && !Array.isArray(data.cities)) {
|
|
674
|
+
data = Object.keys(data.cities).map(key => this._normalizeJsonData({ id: key, ...data.cities[key] }));
|
|
675
|
+
} else if (data.cities && Array.isArray(data.cities) && data.cities.length === 0) {
|
|
676
|
+
data = data.cities;
|
|
677
|
+
} else if (data.newComment && typeof data.newComment === "object") {
|
|
678
|
+
data.newComment = this._normalizeJsonData({ ...data.newComment });
|
|
679
|
+
} else if (Array.isArray(data)) {
|
|
680
|
+
data = data.map((item) => this._normalizeJsonData(item));
|
|
681
|
+
} else if (data.map && typeof data.map === "object") {
|
|
682
|
+
data.map = this._normalizeJsonData(data.map);
|
|
683
|
+
} else if (data.object && typeof data.object === "object") {
|
|
684
|
+
data.object = this._normalizeJsonData(data.object);
|
|
685
|
+
} else if (typeof data === "object" && Object.keys(data).length > 0) {
|
|
686
|
+
// Vérifie si toutes les clés sont des IDs mongo
|
|
687
|
+
const allMongoIds = Object.keys(data).every((key) => /^[0-9a-fA-F]{24}$/.test(key));
|
|
688
|
+
if (allMongoIds) {
|
|
689
|
+
data = Object.keys(data).map((key) => ({
|
|
690
|
+
id: key,
|
|
691
|
+
...this._normalizeJsonData(data[key])
|
|
692
|
+
}));
|
|
693
|
+
} else {
|
|
694
|
+
data = this._normalizeJsonData(data);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return data;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Normalizes JSON data by transforming specific fields and ensuring URLs are complete.
|
|
704
|
+
*
|
|
705
|
+
* @param {Object} item - The JSON object to be normalized.
|
|
706
|
+
* @returns {Object} - The normalized JSON object.
|
|
707
|
+
*
|
|
708
|
+
* The function performs the following transformations:
|
|
709
|
+
* - Normalizes the ID field if it matches a specific pattern.
|
|
710
|
+
* - Converts date fields from seconds to milliseconds.
|
|
711
|
+
* - Ensures URLs in specific fields are complete.
|
|
712
|
+
* - Filters and normalizes nested objects and arrays.
|
|
713
|
+
* - Removes the `timeAgo` field if it exists.
|
|
714
|
+
* - Converts the object to EJSON format if necessary.
|
|
715
|
+
*/
|
|
716
|
+
_normalizeJsonData(item) {
|
|
717
|
+
if (!item || typeof item !== "object") {
|
|
718
|
+
return item;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Application des normalisations récursives
|
|
722
|
+
item = this._normalizeRecursively(item);
|
|
723
|
+
|
|
724
|
+
// item = this._normalizeIdRecursively(item);
|
|
725
|
+
// item = this._normalizeDatesRecursively(item);
|
|
726
|
+
// item = this._normalizeImagesRecursively(item);
|
|
727
|
+
|
|
728
|
+
// if(item?.preferences){
|
|
729
|
+
// item.preferences = this._normalizeBooleansRecursively(item.preferences);
|
|
730
|
+
// }
|
|
731
|
+
|
|
732
|
+
if (item?.mediaImg?.images && Array.isArray(item.mediaImg.images) && item.mediaImg.images.length > 0) {
|
|
733
|
+
item.mediaImg.countImages = item.mediaImg.images.length;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (item?.mediaFile?.files && Array.isArray(item.mediaFile.files) && item.mediaFile.files.length > 0) {
|
|
737
|
+
item.mediaFile.countFiles = item.mediaFile.files.length;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (
|
|
741
|
+
item?.src?.changes &&
|
|
742
|
+
typeof item.src.changes === "object" &&
|
|
743
|
+
Object.keys(item.src.changes).length > 0
|
|
744
|
+
) {
|
|
745
|
+
Object.keys(item.src.changes).forEach((key) => {
|
|
746
|
+
if (item.src.changes[key]) {
|
|
747
|
+
item.src.changes[key] = this._ensureFullURL(item.src.changes[key]);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (item?.openingHours && Array.isArray(item.openingHours) && item.openingHours.length > 0) {
|
|
753
|
+
item.openingHours = item.openingHours.filter(
|
|
754
|
+
(day) => day.dayOfWeek && day.hours && day.hours[0] && day.hours[0].opens
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (item.replies && typeof item.replies === "object" && Object.keys(item.replies).length > 0) {
|
|
759
|
+
item.replies = Object.keys(item.replies).map((key) => {
|
|
760
|
+
return this._normalizeJsonData({ id: key, ...item.replies[key] });
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (
|
|
765
|
+
item?.author &&
|
|
766
|
+
typeof item.author === "object" &&
|
|
767
|
+
Object.keys(item.author).length > 0
|
|
768
|
+
) {
|
|
769
|
+
// Si author contient déjà une propriété attendue (ici "profilThumbImageUrl"),
|
|
770
|
+
// on considère qu'il est déjà aplatit.
|
|
771
|
+
if (!("profilThumbImageUrl" in item.author)) {
|
|
772
|
+
// Sinon, on considère que author est un mapping et on prend le premier sous-objet.
|
|
773
|
+
const firstKey = Object.keys(item.author)[0];
|
|
774
|
+
// On remplace author par le contenu du sous-objet
|
|
775
|
+
item.author = { ...item.author[firstKey] };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Supprime timeAgo
|
|
780
|
+
if (item?.timeAgo) {
|
|
781
|
+
delete item.timeAgo;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Convertit en EJSON si besoin
|
|
785
|
+
return this._fromJSONValue ? EJSON.fromJSONValue(item) : item;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Ensures that the given image path is a full URL. If the image path is already a full URL
|
|
791
|
+
* (i.e., it starts with "http://" or "https://"), it is returned as is. Otherwise, the image path
|
|
792
|
+
* is concatenated with the base URL of the ApiClient instance.
|
|
793
|
+
*
|
|
794
|
+
* @param {string} imagePath - The image path to ensure as a full URL.
|
|
795
|
+
* @returns {string} - The full URL of the image path.
|
|
796
|
+
*/
|
|
797
|
+
_ensureFullURL(imagePath) {
|
|
798
|
+
if (!imagePath) return imagePath;
|
|
799
|
+
imagePath = imagePath.trim();
|
|
800
|
+
if (!imagePath || /^https?:\/\//i.test(imagePath)) {
|
|
801
|
+
return imagePath;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
let separator = "/";
|
|
805
|
+
if (this._baseURL.endsWith("/") && imagePath.startsWith("/")) {
|
|
806
|
+
separator = "";
|
|
807
|
+
} else if (!this._baseURL.endsWith("/") && !imagePath.startsWith("/")) {
|
|
808
|
+
separator = "/";
|
|
809
|
+
} else {
|
|
810
|
+
separator = "";
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return `${this._baseURL}${separator}${imagePath}`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Parcourt récursivement un objet pour normaliser les champs de date.
|
|
818
|
+
* Pour chaque clé présente dans dateFields, si la valeur est un nombre ou un objet
|
|
819
|
+
* contenant une propriété sec (nombre), la transforme en { $date: valeurEnMs }.
|
|
820
|
+
*
|
|
821
|
+
* @param {Object} obj - L’objet à normaliser.
|
|
822
|
+
* @returns {Object} L’objet normalisé.
|
|
823
|
+
*/
|
|
824
|
+
_normalizeDatesRecursively(obj) {
|
|
825
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
826
|
+
|
|
827
|
+
// Si c'est un tableau, on traite chaque élément récursivement.
|
|
828
|
+
if (Array.isArray(obj)) {
|
|
829
|
+
return obj.map(item => this._normalizeDatesRecursively(item));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
Object.keys(obj).forEach((key) => {
|
|
833
|
+
if (this._dateFields.includes(key)) {
|
|
834
|
+
if (obj[key] && typeof obj[key] === "object" && obj[key].sec && typeof obj[key].sec === "number") {
|
|
835
|
+
obj[key] = { $date: obj[key].sec * 1000 };
|
|
836
|
+
} else if (typeof obj[key] === "number") {
|
|
837
|
+
obj[key] = { $date: obj[key] * 1000 };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (obj[key] && typeof obj[key] === "object") {
|
|
841
|
+
this._normalizeDatesRecursively(obj[key]);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
return obj;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Parcourt récursivement un objet pour normaliser les chemins d'images.
|
|
849
|
+
* Pour chaque propriété correspondant à un champ d'image (dans imageFields),
|
|
850
|
+
* vérifie et convertit le chemin en URL complète à l’aide de _ensureFullURL.
|
|
851
|
+
* Les cas particuliers (comme mediaImg.images ou mediaFile.files) sont également gérés.
|
|
852
|
+
*
|
|
853
|
+
* @param {Object} obj - L’objet à normaliser.
|
|
854
|
+
* @returns {Object} L’objet normalisé.
|
|
855
|
+
*/
|
|
856
|
+
_normalizeImagesRecursively(obj) {
|
|
857
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
858
|
+
|
|
859
|
+
// Si c'est un tableau, on traite chaque élément récursivement.
|
|
860
|
+
if (Array.isArray(obj)) {
|
|
861
|
+
return obj.map(item => this._normalizeImagesRecursively(item));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
Object.keys(obj).forEach((key) => {
|
|
865
|
+
if (this._imageFields.includes(key) && typeof obj[key] === "string" && obj[key].trim() !== "") {
|
|
866
|
+
obj[key] = this._ensureFullURL(obj[key]);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Cas particulier pour content.image
|
|
870
|
+
if (key === "content" && obj[key]?.image) {
|
|
871
|
+
obj[key].image = this._ensureFullURL(obj[key].image);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (obj[key] && typeof obj[key] === "object") {
|
|
875
|
+
this._normalizeImagesRecursively(obj[key]);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
return obj;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Parcourt récursivement un objet ou un tableau pour normaliser les identifiants (ID).
|
|
883
|
+
* Si un objet possède une propriété "_id" ou "id" contenant un sous-objet "$id" valide,
|
|
884
|
+
* l'ID est normalisé et la propriété _id est convertie au format EJSON attendu.
|
|
885
|
+
*
|
|
886
|
+
* @param {Object|Array} obj - L'objet ou le tableau à normaliser.
|
|
887
|
+
* @returns {Object|Array} L'objet ou le tableau normalisé.
|
|
888
|
+
*/
|
|
889
|
+
_normalizeIdRecursively(obj) {
|
|
890
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
891
|
+
|
|
892
|
+
// Si c'est un tableau, on traite chaque élément récursivement.
|
|
893
|
+
if (Array.isArray(obj)) {
|
|
894
|
+
return obj.map(item => this._normalizeIdRecursively(item));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Vérifier et normaliser l'ID dans l'objet
|
|
898
|
+
if (obj._id && obj._id.$id && /^[0-9a-fA-F]{24}$/.test(obj._id.$id)) {
|
|
899
|
+
obj.id = obj._id.$id;
|
|
900
|
+
obj._id = { $type: "oid", $value: obj._id.$id };
|
|
901
|
+
}
|
|
902
|
+
if (obj.id && obj.id.$id && /^[0-9a-fA-F]{24}$/.test(obj.id.$id)) {
|
|
903
|
+
obj._id = { $type: "oid", $value: obj.id.$id };
|
|
904
|
+
obj.id = obj.id.$id;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Appel récursif pour chaque propriété de l'objet
|
|
908
|
+
Object.keys(obj).forEach(key => {
|
|
909
|
+
if (obj[key] && typeof obj[key] === "object") {
|
|
910
|
+
obj[key] = this._normalizeIdRecursively(obj[key]);
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
return obj;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Normalise récursivement les valeurs booléennes.
|
|
919
|
+
* Si une valeur est une chaîne "true" ou "false", elle est convertie en booléen.
|
|
920
|
+
*
|
|
921
|
+
* @param {any} data - L'objet, le tableau ou la valeur à normaliser.
|
|
922
|
+
* @returns {any} La donnée normalisée.
|
|
923
|
+
*/
|
|
924
|
+
_normalizeBooleansRecursively(data) {
|
|
925
|
+
if (typeof data === "string") {
|
|
926
|
+
if (data === "true") return true;
|
|
927
|
+
if (data === "false") return false;
|
|
928
|
+
return data;
|
|
929
|
+
}
|
|
930
|
+
if (Array.isArray(data)) {
|
|
931
|
+
return data.map(item => this._normalizeBooleansRecursively(item));
|
|
932
|
+
}
|
|
933
|
+
if (data !== null && typeof data === "object") {
|
|
934
|
+
Object.keys(data).forEach(key => {
|
|
935
|
+
data[key] = this._normalizeBooleansRecursively(data[key]);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return data;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Transforme une chaîne "true" ou "false" en booléen.
|
|
943
|
+
*
|
|
944
|
+
* @param {string} str - La chaîne à normaliser.
|
|
945
|
+
* @returns {boolean|string} La valeur booléenne ou la chaîne originale.
|
|
946
|
+
*/
|
|
947
|
+
_normalizeString(str) {
|
|
948
|
+
if (str === "true") return true;
|
|
949
|
+
if (str === "false") return false;
|
|
950
|
+
return str;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Normalise les identifiants dans un objet.
|
|
955
|
+
* Si l'objet possède une propriété "_id" ou "id" contenant un sous-objet "$id" valide,
|
|
956
|
+
* il met à jour l'objet en assignant la valeur de l'ID et en formattant _id en EJSON.
|
|
957
|
+
*
|
|
958
|
+
* @param {Object} obj - L'objet à normaliser.
|
|
959
|
+
* @returns {Object} L'objet avec l'ID normalisé.
|
|
960
|
+
*/
|
|
961
|
+
_normalizeId(obj) {
|
|
962
|
+
if (obj._id && obj._id.$id && /^[0-9a-fA-F]{24}$/.test(obj._id.$id)) {
|
|
963
|
+
obj.id = obj._id.$id;
|
|
964
|
+
obj._id = { $type: "oid", $value: obj._id.$id };
|
|
965
|
+
}
|
|
966
|
+
if (obj.id && obj.id.$id && /^[0-9a-fA-F]{24}$/.test(obj.id.$id)) {
|
|
967
|
+
obj._id = { $type: "oid", $value: obj.id.$id };
|
|
968
|
+
obj.id = obj.id.$id;
|
|
969
|
+
}
|
|
970
|
+
return obj;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Normalise une valeur de date.
|
|
975
|
+
* Si la valeur est un objet contenant "sec" (nombre) ou un nombre,
|
|
976
|
+
* elle est convertie en { $date: valeurEnMs }.
|
|
977
|
+
*
|
|
978
|
+
* @param {any} value - La valeur à normaliser.
|
|
979
|
+
* @returns {any} La valeur normalisée.
|
|
980
|
+
*/
|
|
981
|
+
_normalizeDate(value) {
|
|
982
|
+
if (value && typeof value === "object" && typeof value.sec === "number") {
|
|
983
|
+
return { $date: value.sec * 1000 };
|
|
984
|
+
} else if (typeof value === "number") {
|
|
985
|
+
return { $date: value * 1000 };
|
|
986
|
+
}
|
|
987
|
+
return value;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Normalise une URL d'image.
|
|
992
|
+
* Si la valeur est une chaîne non vide, retourne l'URL complète via _ensureFullURL.
|
|
993
|
+
*
|
|
994
|
+
* @param {any} value - La valeur à normaliser.
|
|
995
|
+
* @returns {any} La valeur normalisée.
|
|
996
|
+
*/
|
|
997
|
+
_normalizeImage(value) {
|
|
998
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
999
|
+
return this._ensureFullURL(value);
|
|
1000
|
+
}
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Liste des champs d'image à normaliser.
|
|
1006
|
+
*/
|
|
1007
|
+
_imageFields = [
|
|
1008
|
+
"profilImageUrl",
|
|
1009
|
+
"profilThumbImageUrl",
|
|
1010
|
+
"profilMediumImageUrl",
|
|
1011
|
+
"profilMarkerImageUrl",
|
|
1012
|
+
"profilBannerUrl",
|
|
1013
|
+
"profilRealBannerUrl",
|
|
1014
|
+
"imagePath",
|
|
1015
|
+
"imageThumbPath",
|
|
1016
|
+
"docPath"
|
|
1017
|
+
];
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Liste des champs de date à normaliser.
|
|
1021
|
+
*/
|
|
1022
|
+
_dateFields = ["modified", "created", "updated", "birthDate", "lastLoginDate", "startDate", "endDate", "date"];
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Normalise récursivement un objet, un tableau ou une valeur simple.
|
|
1026
|
+
* Cette fonction réalise en une seule passe les opérations suivantes :
|
|
1027
|
+
* - Conversion des chaînes "true"/"false" en booléens.
|
|
1028
|
+
* - Normalisation des dates (pour les champs spécifiés).
|
|
1029
|
+
* - Normalisation des images (pour les champs spécifiés).
|
|
1030
|
+
* - Transformation récursive des objets et tableaux.
|
|
1031
|
+
* - Normalisation des identifiants.
|
|
1032
|
+
*
|
|
1033
|
+
* @param {any} data - La donnée à normaliser.
|
|
1034
|
+
* @returns {any} La donnée normalisée.
|
|
1035
|
+
*/
|
|
1036
|
+
_normalizeRecursively(data) {
|
|
1037
|
+
// Cas de base pour les chaînes
|
|
1038
|
+
if (typeof data === "string") {
|
|
1039
|
+
return this._normalizeString(data);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// Traitement des tableaux
|
|
1043
|
+
if (Array.isArray(data)) {
|
|
1044
|
+
return data.map(item => this._normalizeRecursively(item));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Traitement des objets non nuls
|
|
1048
|
+
if (data !== null && typeof data === "object") {
|
|
1049
|
+
// On crée une copie de l'objet pour éviter les effets de bord.
|
|
1050
|
+
const normalizedData = {};
|
|
1051
|
+
|
|
1052
|
+
Object.keys(data).forEach(key => {
|
|
1053
|
+
// Appliquer récursivement la normalisation sur la valeur.
|
|
1054
|
+
let value = this._normalizeRecursively(data[key]);
|
|
1055
|
+
|
|
1056
|
+
// Si la clé correspond à un champ de date, on applique la normalisation de date.
|
|
1057
|
+
if (this._dateFields.includes(key)) {
|
|
1058
|
+
value = this._normalizeDate(value);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Si la clé correspond à un champ d'image, on applique la normalisation d'image.
|
|
1062
|
+
if (this._imageFields.includes(key)) {
|
|
1063
|
+
value = this._normalizeImage(value);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Cas particulier : si la clé est "content" et qu'elle possède une propriété "image".
|
|
1067
|
+
if (key === "content" && value && typeof value === "object" && value.image) {
|
|
1068
|
+
value.image = this._normalizeImage(value.image);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
normalizedData[key] = value;
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// Normalisation des identifiants après traitement de l'objet.
|
|
1075
|
+
return this._normalizeId(normalizedData);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Pour les autres types (nombres, booléens déjà, etc.), on renvoie la donnée inchangée.
|
|
1079
|
+
return data;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
|
|
1083
|
+
}
|