@communecter/cocolight-api-client 1.0.20 → 1.0.21

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.
@@ -1,40 +1,72 @@
1
1
  // BaseEntity.js
2
- import { ApiError } from "../error.js";
3
- import { DraftStateMixin } from "../mixin/DraftStateMixin.js";
4
- import { EntityMixin } from "../mixin/EntityMixin.js";
5
- import { MutualEntityMixin } from "../mixin/MutualEntityMixin.js";
6
- import { UtilMixin } from "../mixin/UtilMixin.js";
2
+ import ObjectID from "bson-objectid";
3
+ // import { fileTypeFromBuffer } from "file-type";
4
+ import pkg from "file-type";
7
5
 
6
+ import { ApiAuthenticationError, ApiError, ApiResponseError, ApiValidationError } from "../error.js";
7
+ const { fromBuffer } = pkg;
8
+
9
+
10
+ /**
11
+ * Classe de base pour toutes les entités métiers : utilisateurs, projets, organisations, etc.
12
+ * Fournit un système de brouillon (draft), transformation, appel API sécurisé,
13
+ * et gestion de données côté client avec support du mode offline.
14
+ * @abstract
15
+ */
8
16
  export class BaseEntity {
17
+ /** @type {Object} Données de brouillon modifiables */
9
18
  _draftData = {};
19
+
20
+ /** @type {Object} Snapshot initial des données de brouillon */
10
21
  _initialDraftData = {};
22
+
23
+ /** @type {Object|null} Données reçues du serveur */
11
24
  _serverData = null;
25
+
26
+ /** @type {boolean} Indique si `save()` est en cours */
12
27
  _calledFromSave = false;
13
28
 
29
+ /**
30
+ * Constructeur de l'entité.
31
+ * @param {Object} parent - L'ApiClient ou une entité parente.
32
+ * @param {Object} parent.apiClient - Instance de l'ApiClient.
33
+ * @param {Object} [parent.parent] - Instance parente.
34
+ * @param {Object} [data={}] - Données initiales.
35
+ * @param {Object} [deps={}] - Dépendances injectées (EndpointApi, autres entités).
36
+ * @param {Object|function} deps.EndpointApi - Instance de l'API.
37
+ * @param {function} deps.User - Classe d'entité utilisateur.
38
+ * @param {function} deps.Organization - Classe d'entité organisation.
39
+ * @param {function} deps.Project - Classe d'entité projet.
40
+ * @param {function} deps.Poi - Classe d'entité point d'intérêt.
41
+ * @param {function} deps.Event - Classe d'entité événement.
42
+ * @param {function} deps.Badge - Classe d'entité badge.
43
+ * @param {function} deps.News - Classe d'entité actualité.
44
+ * @param {string} [config.entityTag] - Tag d'entité (ex: "User", "Organization").
45
+ * @param {Object} [config={}] - Configuration optionnelle.
46
+ * @throws {ApiError} Si les dépendances ou parent sont invalides.
47
+ */
14
48
  constructor(parent, data = {}, deps = {}, config = {}) {
15
49
  this.__entityTag = config.entityTag || this.constructor.name || "BaseEntity";
16
50
  this.deps = deps;
17
51
 
18
- // this.EndpointApiClass = deps.EndpointApi;
19
-
20
52
  if (parent?.__entityTag === "ApiClient") {
53
+ /** @type {import("../ApiClient.js").default} */
21
54
  this.apiClient = parent;
22
55
  this.parent = null;
23
56
  } else if (parent?.apiClient) {
57
+ /** @type {import("../ApiClient.js").default} */
24
58
  this.apiClient = parent.apiClient;
25
59
  this.parent = parent;
26
60
  } else {
27
61
  throw new ApiError("Parent invalide ou ApiClient manquant.");
28
62
  }
29
63
 
30
- // this.endpointApi = typeof deps.EndpointApi === "function"
31
- // ? new deps.EndpointApi(this.apiClient)
32
- // : deps.EndpointApi;
33
-
34
64
  // Gérer les deux cas : fonction constructeur ou instance
35
65
  if (typeof deps.EndpointApi === "function") {
66
+ /** @type {import("./EndpointApi.js").default} */
36
67
  this.endpointApi = new deps.EndpointApi(this.apiClient);
37
68
  } else if (typeof deps.EndpointApi === "object") {
69
+ /** @type {import("./EndpointApi.js").default} */
38
70
  this.endpointApi = deps.EndpointApi;
39
71
  } else {
40
72
  throw new ApiError("deps.EndpointApi doit être une classe ou une instance valide.");
@@ -56,63 +88,72 @@ export class BaseEntity {
56
88
  this.data = proxy;
57
89
  }
58
90
 
91
+ /** @returns {string|null} Identifiant de l'entité */
59
92
  get id() {
60
93
  return this._draftData.id || null;
61
94
  }
62
95
 
96
+ /** Définit un ID (utilisé en interne) */
63
97
  _id(newId) {
64
98
  this._draftData.id = newId;
65
99
  }
66
100
 
101
+ /** @returns {boolean} Indique si l'utilisateur est connecté */
67
102
  get isConnected() {
68
103
  return this.apiClient.isConnected;
69
104
  }
70
105
 
106
+ /** @returns {string|null} Identifiant utilisateur associé */
71
107
  get userId() {
72
108
  return this.apiClient.userId;
73
109
  }
74
110
 
111
+ /** @returns {Object} Données de brouillon courantes */
75
112
  get draftData() {
76
113
  return this._draftData;
77
114
  }
78
115
 
116
+ /** @returns {Object} Données de brouillon initiales */
79
117
  get initialDraftData() {
80
118
  return this._initialDraftData;
81
119
  }
82
120
 
121
+ /** @returns {Object|null} Données brutes du serveur */
83
122
  get serverData() {
84
123
  return this._serverData;
85
124
  }
86
125
 
126
+ /** @returns {boolean} Indique si cette entité représente l'utilisateur connecté */
87
127
  get isMe() {
88
128
  return this.isConnected && this.userId === this.parent?.id;
89
129
  }
90
130
 
131
+ /** @returns {string} Type de l'entité (ex: 'citoyens') */
91
132
  getEntityType() {
92
133
  return this.constructor.entityType;
93
134
  }
94
135
 
95
- _setData(newData) {
96
- this._serverData = { ...newData };
97
-
98
- const { draft, proxy } = this._buildDraftAndProxy({
99
- data: { ...newData, ...this.defaultFields },
100
- serverData: this._serverData,
101
- constant: this.constructor.SCHEMA_CONSTANTS,
102
- apiClient: this.apiClient,
103
- transforms: this.transforms,
104
- removeFields: this.removeFields
105
- });
106
- this._initialDraftData = JSON.parse(JSON.stringify(draft));
107
- this._draftData = draft;
108
- this.data = proxy;
136
+ /**
137
+ * Indique si le draft contient des modifications par rapport aux données initiales.
138
+ * @returns {boolean}
139
+ */
140
+ hasChanges() {
141
+ return JSON.stringify(this._draftData) !== JSON.stringify(this._initialDraftData);
109
142
  }
110
143
 
144
+ /**
145
+ * Rafraîchit l'entité en rechargeant ses données depuis le serveur.
146
+ * @returns {Promise<Object>} Données mises à jour
147
+ */
111
148
  async refresh() {
112
149
  if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.");
113
150
  return this.get();
114
151
  }
115
152
 
153
+ /**
154
+ * Sauvegarde les modifications locales vers le serveur (add ou update).
155
+ * @returns {Promise<Object>} Données serveur mises à jour
156
+ */
116
157
  async save() {
117
158
  if (!this.isConnected) throw new ApiError("Non connecté.");
118
159
  this._calledFromSave = true;
@@ -133,6 +174,14 @@ export class BaseEntity {
133
174
  }
134
175
  }
135
176
 
177
+ /**
178
+ * Crée une nouvelle instance d'entité à partir des données du serveur.
179
+ *
180
+ * @param {Object} data - Données du serveur.
181
+ * @param {BaseEntity} parent - Instance parente.
182
+ * @param {Object} deps - Dépendances injectées.
183
+ * @returns {BaseEntity} Nouvelle instance d'entité.
184
+ */
136
185
  static fromServerData(data, parent, deps) {
137
186
  const instance = new this(parent, {}, deps);
138
187
  instance._serverData = { ...data };
@@ -151,21 +200,1349 @@ export class BaseEntity {
151
200
  return instance;
152
201
  }
153
202
 
154
- hasChanges() {
155
- return JSON.stringify(this._draftData) !== JSON.stringify(this._initialDraftData);
203
+ /**
204
+ * Met à jour les données de l'entité avec de nouvelles données.
205
+ *
206
+ * @param {Object} newData - Les nouvelles données à appliquer.
207
+ * @returns {void}
208
+ * @private
209
+ */
210
+ _setData(newData) {
211
+ this._serverData = { ...newData };
212
+
213
+ const { draft, proxy } = this._buildDraftAndProxy({
214
+ data: { ...newData, ...this.defaultFields },
215
+ serverData: this._serverData,
216
+ constant: this.constructor.SCHEMA_CONSTANTS,
217
+ apiClient: this.apiClient,
218
+ transforms: this.transforms,
219
+ removeFields: this.removeFields
220
+ });
221
+ this._initialDraftData = JSON.parse(JSON.stringify(draft));
222
+ this._draftData = draft;
223
+ this.data = proxy;
156
224
  }
157
225
 
226
+
227
+ /**
228
+ * Champs à ajouter automatiquement à chaque draft (ex: `typeElement`).
229
+ * Souvent utilisés dans les conditions `if/then` du JSON Schema.
230
+ * @type {Object<string, any>}
231
+ */
158
232
  defaultFields = {};
233
+
234
+ /**
235
+ * Champs à exclure explicitement du draft et des payloads.
236
+ * @type {string[]}
237
+ */
159
238
  removeFields = [];
239
+
240
+ /**
241
+ * Transformations à appliquer à certains champs lors de la lecture depuis le draft.
242
+ * Clé = champ, valeur = fonction (val, full) => valeur transformée.
243
+ * @type {Object<string, function(any, object): any>}
244
+ */
160
245
  transforms = {};
161
- }
162
246
 
163
- Object.assign(
164
- BaseEntity.prototype,
165
- DraftStateMixin,
166
- EntityMixin,
167
- MutualEntityMixin,
168
- UtilMixin
169
- );
247
+
248
+ /**
249
+ * ───────────────────────────────
250
+ * UtilMixin
251
+ * ───────────────────────────────
252
+ */
253
+
254
+ /**
255
+ * Appelle une méthode de l'API.
256
+ *
257
+ * @param {string} constant - Le nom de la méthode à appeler.
258
+ * @param {object} data - Les données à passer à la méthode.
259
+ * @returns {Promise<any>} - La promesse de la méthode appelée.
260
+ * @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
261
+ * @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
262
+ * @throws {ApiClientError} - Si l'utilisateur n'est pas authentifié.
263
+ */
264
+ async call(constant, data = {}) {
265
+ return this.apiClient.safeCall(async () => {
266
+ const response = await this.apiClient.callEndpoint(constant, data);
267
+ this.apiClient.checkAndThrowApiResponseError(response);
268
+ return response.data;
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Appelle une méthode de l'API si l'utilisateur n'est pas connecté.
274
+ *
275
+ * @param {string} constant - Le nom de la méthode à appeler.
276
+ * @param {object} data - Les données à passer à la méthode.
277
+ * @returns {Promise<any>} - La promesse de la méthode appelée.
278
+ * @throws {ApiAuthenticationError} - Si l'utilisateur est connecté.
279
+ * @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
280
+ * @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
281
+ * @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
282
+ */
283
+ async callNoConnected(constant, data = {}) {
284
+ if(this.isConnected) {
285
+ throw new ApiAuthenticationError("Vous devez ne devez pas être connecté pour faire cette action.");
286
+ }
287
+ return this.call(constant, data);
288
+ }
289
+
290
+ /**
291
+ * Appelle une méthode de l'API si l'utilisateur est connecté.
292
+ *
293
+ * @param {string|function} param - Le nom de la méthode à appeler ou une fonction de rappel.
294
+ * @param {object} data - Les données à passer à la méthode.
295
+ * @returns {Promise<any>} - La promesse de la méthode appelée.
296
+ * @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas connecté.
297
+ * @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
298
+ * @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
299
+ * @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
300
+ */
301
+ async callIsConnected(param, data = {}) {
302
+ if(!this.isConnected) {
303
+ throw new ApiAuthenticationError("Vous devez être connecté pour faire cette action.");
304
+ }
305
+ // Si le premier paramètre est une fonction, on l'exécute en tant que callback
306
+ if (typeof param === "function") {
307
+ return await param();
308
+ }
309
+ return this.call(param, data);
310
+ }
311
+
312
+ /**
313
+ * Appelle une méthode de l'API si l'utilisateur est lui-même.
314
+ *
315
+ * @param {string|function} param - Le nom de la méthode à appeler ou une fonction de rappel.
316
+ * @param {object} data - Les données à passer à la méthode.
317
+ * @returns {Promise<any>} - La promesse de la méthode appelée.
318
+ * @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas lui-même.
319
+ * @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
320
+ * @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
321
+ * @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
322
+ */
323
+ async callIsMe(param, data = {}) {
324
+ if (!this.isMe) {
325
+ throw new ApiAuthenticationError("Vous devez être vous-même pour faire cette action.");
326
+ }
327
+ // Si le premier paramètre est une fonction, on l'exécute en tant que callback
328
+ if (typeof param === "function") {
329
+ return await param();
330
+ }
331
+ // Sinon, on considère qu'il s'agit d'un constant et on appelle la méthode par défaut
332
+ return await this.callIsConnected(param, data);
333
+ }
334
+
335
+ /**
336
+ * Valide une image d'entrée.
337
+ *
338
+ * @param {File|Blob|Buffer|ReadableStream} imageInput - L'image à valider.
339
+ * @returns {Promise<File|Buffer|ReadableStream>} - L'image validée.
340
+ * @private
341
+ */
342
+ async _validateImage(imageInput){
343
+ const image = await this._validateUploadInput(imageInput, {
344
+ allowedMimeTypes: ["image/jpeg", "image/png", "image/jpg"],
345
+ expectedType: "image"
346
+ });
347
+ return image;
348
+ }
349
+
350
+ /**
351
+ * Valide un fichier d'entrée.
352
+ *
353
+ * @param {File|Blob|Buffer|ReadableStream} fileInput - Le fichier à valider.
354
+ * @returns {Promise<File|Buffer|ReadableStream>} - Le fichier validé.
355
+ * @private
356
+ */
357
+ async _validateFile(fileInput){
358
+ const file = await this._validateUploadInput(fileInput, {
359
+ allowedMimeTypes: ["application/pdf", "text/plain", "text/csv"],
360
+ expectedType: "file"
361
+ });
362
+ return file;
363
+ }
364
+
365
+ /**
366
+ * Valide les entrées d'upload de fichiers.
367
+ *
368
+ * @param {File|Blob|Buffer|ReadableStream} input - Le fichier à valider.
369
+ * @param {Object} options - Options de validation.
370
+ * @param {Array} options.allowedMimeTypes - Types MIME autorisés.
371
+ * @param {string} options.expectedType - Type de fichier attendu (ex: "image", "file").
372
+ * @returns {Promise<File|Buffer|ReadableStream>} - Le fichier validé.
373
+ * @throws {ApiValidationError} - Si le type de fichier est invalide.
374
+ * @throws {Error} - Si le type de fichier est inconnu.
375
+ * @private
376
+ */
377
+ async _validateUploadInput(input, { allowedMimeTypes = [], expectedType = "any" }) {
378
+ if (!input) {
379
+ throw new ApiValidationError("Le fichier est requis.");
380
+ }
381
+
382
+ const isNode = typeof window === "undefined" && typeof process !== "undefined";
383
+ let mimeType = "";
384
+ let output = input;
385
+
386
+ // Navigateur : File
387
+ if (typeof File !== "undefined" && input instanceof File) {
388
+ mimeType = input.type;
389
+ if (!allowedMimeTypes.includes(mimeType)) {
390
+ throw new ApiValidationError("Le type du fichier est invalide.");
391
+ }
392
+ }
393
+
394
+ // Navigateur : Blob
395
+ else if (typeof Blob !== "undefined" && input instanceof Blob) {
396
+ mimeType = input.type;
397
+ if (!allowedMimeTypes.includes(mimeType)) {
398
+ throw new ApiValidationError("Le type du fichier est invalide.");
399
+ }
400
+
401
+ const ext = mimeType.split("/")[1] || "bin";
402
+ const fileName = `${Date.now()}.${ext}`;
403
+ output = new File([input], fileName, { type: mimeType });
404
+ }
405
+
406
+ // Node.js : Buffer
407
+ else if (isNode && Buffer.isBuffer(input)) {
408
+ const fileTypeResult = await fromBuffer(input);
409
+ mimeType = fileTypeResult?.mime;
410
+
411
+ if (!fileTypeResult || !allowedMimeTypes.includes(mimeType)) {
412
+ throw new ApiValidationError("Le type du fichier est invalide.");
413
+ }
414
+
415
+ // Pour un fichier image, on transforme en stream
416
+ if (expectedType === "image") {
417
+ const ext = fileTypeResult.ext;
418
+ const filename = `${Date.now()}.${ext}`;
419
+ output = await this._createReadStreamFromBuffer(input, filename, mimeType);
420
+ }
421
+ }
422
+
423
+ // Node.js : ReadableStream
424
+ else if (isNode && input?.readable && typeof input._read === "function") {
425
+ const previewChunks = [];
426
+ const tee = await this._passThrough();
427
+ const resultStream = await this._passThrough();
428
+
429
+ const MAX_BYTES = 4100;
430
+ let bytesRead = 0;
431
+
432
+ input.on("data", (chunk) => {
433
+ if (bytesRead < MAX_BYTES) {
434
+ previewChunks.push(chunk);
435
+ bytesRead += chunk.length;
436
+ }
437
+ });
438
+
439
+ input.pipe(tee).pipe(resultStream);
440
+ await new Promise((resolve) => setTimeout(resolve, 10));
441
+
442
+ const previewBuffer = Buffer.concat(previewChunks);
443
+ const fileTypeResult = await fromBuffer(previewBuffer);
444
+ mimeType = fileTypeResult?.mime;
445
+
446
+ if (!fileTypeResult || !allowedMimeTypes.includes(mimeType)) {
447
+ throw new ApiValidationError("Le type du fichier est invalide.");
448
+ }
449
+
450
+ resultStream.path = `${Date.now()}.${fileTypeResult.ext}`;
451
+ resultStream.mimeType = mimeType;
452
+ output = resultStream;
453
+ }
454
+
455
+ else {
456
+ throw new ApiValidationError("Type de fichier non reconnu.");
457
+ }
458
+
459
+ return output;
460
+ }
461
+
462
+ /**
463
+ * Transforme un Buffer en ReadableStream équivalent à fs.createReadStream
464
+ * @param {Buffer} buffer - Le buffer contenant les données binaires
465
+ * @param {string} filename - Nom de fichier (utilisé dans FormData)
466
+ * @param {string} mimeType - Type MIME (utilisé dans FormData)
467
+ * @returns {Object} - { stream, filename, mimeType }
468
+ * @private
469
+ */
470
+ async _createReadStreamFromBuffer(buffer, filename = "file.bin", mimeType = "application/octet-stream") {
471
+ const stream = await this._bufferToReadable(buffer);
472
+ stream.path = filename; // 👈 hack pour simuler un vrai fichier ReadStream
473
+ stream.mimeType = mimeType;
474
+ return stream;
475
+ }
476
+
477
+ /**
478
+ * Transforme un Buffer en ReadableStream.
479
+ *
480
+ * @param {Buffer} buffer - Le buffer à transformer.
481
+ * @returns {Promise<stream.Readable>} - Un ReadableStream.
482
+ * @throws {Error} - Si appelé dans le navigateur.
483
+ * @private
484
+ */
485
+ async _bufferToReadable(buffer) {
486
+ if (typeof window === "undefined") {
487
+ const { bufferToReadable } = await import("../utils/stream-utils.node.js");
488
+ return bufferToReadable(buffer);
489
+ } else {
490
+ throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Crée un PassThrough stream pour le traitement des fichiers.
496
+ *
497
+ * @returns {Promise<stream.PassThrough>} - Un PassThrough stream.
498
+ * @throws {Error} - Si appelé dans le navigateur.
499
+ * @private
500
+ */
501
+ async _passThrough() {
502
+ if (typeof window === "undefined") {
503
+ const { createPassThrough } = await import("../utils/stream-utils.node.js");
504
+ return createPassThrough();
505
+ } else {
506
+ throw new Error("passThrough ne doit pas être appelé dans le navigateur");
507
+ }
508
+ }
509
+
510
+ // async _bufferToReadable(buffer) {
511
+ // if (typeof window === "undefined") {
512
+ // const { Readable } = await import("stream");
513
+ // return Readable.from(buffer);
514
+ // } else {
515
+ // throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
516
+ // }
517
+ // },
518
+
519
+ // async _passThrough() {
520
+ // if (typeof window === "undefined") {
521
+ // const { PassThrough } = await import("stream");
522
+ // return new PassThrough();
523
+ // } else {
524
+ // throw new Error("passThrough ne doit pas être appelé dans le navigateur");
525
+ // }
526
+ // },
527
+
528
+ /**
529
+ * Supprime les propriétés d'un objet en fonction d'une liste de clés.
530
+ *
531
+ * @param {Object} obj - L'objet source.
532
+ * @param {Array} propsToRemove - Liste des clés à supprimer
533
+ * @param {boolean} [deep=false] - Si vrai, supprime les propriétés de manière récursive.
534
+ * @returns {Object} - Un nouvel objet sans les propriétés supprimées.
535
+ * @private
536
+ */
537
+ _omitProps(obj, propsToRemove) {
538
+ if (!obj || typeof obj !== "object") return {};
539
+
540
+ const result = { ...obj };
541
+ for (const prop of propsToRemove) {
542
+ delete result[prop];
543
+ }
544
+ return result;
545
+ }
546
+
547
+ /**
548
+ * Extrait les propriétés d'un objet en fonction d'une liste de clés.
549
+ *
550
+ * @param {Object} obj - L'objet source.
551
+ * @param {Array} keys - Liste des clés à extraire.
552
+ * @param {Object} [transforms={}] - Transformations à appliquer aux valeurs.
553
+ * @returns {Object} - Un nouvel objet contenant les propriétés extraites.
554
+ * @private
555
+ */
556
+ _pickProps(obj, keys, transforms = {}) {
557
+ if (!obj || typeof obj !== "object") return {};
558
+
559
+ const result = {};
560
+
561
+ for (const key of keys) {
562
+ if (key in obj) {
563
+ const value = obj[key];
564
+ if (typeof transforms[key] === "function") {
565
+ result[key] = transforms[key](value, obj); // (valeur, objet source)
566
+ } else {
567
+ result[key] = value;
568
+ }
569
+ }
570
+ }
571
+
572
+ return result;
573
+ }
574
+
575
+ /**
576
+ * Vérifie si au moins une clé est présente dans l'objet et n'est pas nulle.
577
+ *
578
+ * @param {Object} obj - L'objet à vérifier.
579
+ * @param {Array} keys - Liste des clés à vérifier.
580
+ * @returns {boolean} - true si au moins une clé est présente et non nulle, sinon false.
581
+ * @private
582
+ */
583
+ _hasAtLeastOne(obj, keys = []) {
584
+ return keys.some((key) => key in obj && obj[key] != null);
585
+ }
586
+
587
+ /**
588
+ * Crée un proxy filtré pour une liste d'entités.
589
+ * @param {Array} list - Liste d'entités.
590
+ * @returns {Proxy} Proxy filtré.
591
+ * @private
592
+ */
593
+ _createFilteredProxy(list) {
594
+ return new Proxy(list, {
595
+ get(target, prop, receiver) {
596
+ if (typeof prop === "string" && !isNaN(prop)) {
597
+ const active = target.filter(n => !n._isDeleted);
598
+ return active[prop];
599
+ }
600
+ if (prop === "length") {
601
+ return target.filter(n => !n._isDeleted).length;
602
+ }
603
+ if (typeof target[prop] === "function") {
604
+ return (...args) => target.filter(n => !n._isDeleted)[prop](...args);
605
+ }
606
+ return Reflect.get(target, prop, receiver);
607
+ }
608
+ });
609
+ }
610
+
611
+ /**
612
+ * Génère un nouvel identifiant unique.
613
+ * @returns {string} Un identifiant unique.
614
+ * @private
615
+ */
616
+ _newId() {
617
+ const newId = new ObjectID();
618
+ return newId.toString();
619
+ }
620
+
621
+ /**
622
+ * ───────────────────────────────
623
+ * DraftStateMixin
624
+ * ───────────────────────────────
625
+ */
626
+
627
+ /**
628
+ * Crée un proxy combinant draft + serveur, avec transformations facultatives.
629
+ * @param {Object} server
630
+ * @param {Object} draft
631
+ * @param {Array} allowedFields - champs autorisés dans le draft
632
+ * @param {Object} [transforms={}] - transformateurs de lecture
633
+ * @param {Object} [options={}] - options
634
+ * @returns {Proxy}
635
+ * @private
636
+ */
637
+ _createDraftProxy(apiClient, server = {}, draft = {}, allowedFields = [], transforms = {}, options = {}) {
638
+ return new Proxy({}, {
639
+ get: (_, prop) => {
640
+ const val = prop in draft ? draft[prop] : server[prop];
641
+ const transformer = transforms[prop];
642
+ return typeof transformer === "function" ? transformer(val) : val;
643
+ },
644
+
645
+ set: (_, prop, value) => {
646
+ if (!allowedFields.includes(prop)) {
647
+ const message = `[DraftProxy] Le champ "${prop}" n'est pas autorisé.`;
648
+ if (options.throwOnError) {
649
+ throw new ApiValidationError(message, 400, null, {
650
+ field: prop,
651
+ value,
652
+ allowedFields
653
+ });
654
+ }
655
+ apiClient._logger.warn(message);
656
+ return false;
657
+ }
658
+ draft[prop] = value;
659
+ return true;
660
+ },
661
+
662
+ deleteProperty: (_, prop) => {
663
+ if (!allowedFields.includes(prop)) return false;
664
+ delete draft[prop];
665
+ return true;
666
+ },
667
+
668
+ has: (_, prop) => prop in draft || prop in server,
669
+ ownKeys: () => [...new Set([...Object.keys(server), ...Object.keys(draft)])],
670
+ getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true })
671
+ });
672
+ }
673
+
674
+ /**
675
+ * Extrait les champs modifiables d'un schéma JSON.
676
+ *
677
+ * @param {Object} schema - Le schéma JSON à analyser.
678
+ * @param {Object} data - Les données à comparer.
679
+ * @param {Object} ctx - Contexte d'extraction (pour la récursion).
680
+ * @param {Object} ctx.defs - Définitions de schéma.
681
+ * @param {Set} ctx.visited - Ensemble des schémas déjà visités.
682
+ * @returns {Array} - Liste des champs modifiables.
683
+ * @private
684
+ */
685
+ _extractWritableFields(schema = {}, data = {}, ctx = { defs: {}, visited: new Set() }) {
686
+ if (!schema || typeof schema !== "object") return [];
687
+ if (schema.$id && ctx.visited.has(schema.$id)) return [];
688
+ if (schema.$id) ctx.visited.add(schema.$id);
689
+ ctx.defs = schema.$defs || schema.definitions || ctx.defs;
690
+
691
+ const fields = [];
692
+
693
+ if (schema.$ref) {
694
+ const refKey = schema.$ref.replace(/^#\/?(\$defs|definitions)\//, "");
695
+ const resolved = ctx.defs?.[refKey];
696
+ if (resolved) fields.push(...this._extractWritableFields(resolved, data, ctx));
697
+ }
698
+
699
+ if (schema.allOf) {
700
+ schema.allOf.forEach(s => fields.push(...this._extractWritableFields(s, data, ctx)));
701
+ }
702
+
703
+ if (schema.if && schema.then) {
704
+ const condition = schema.if?.properties;
705
+ let matches = true;
706
+ if (condition) {
707
+ for (const key in condition) {
708
+ const expected = condition[key]?.const;
709
+ if (data[key] !== expected) {
710
+ matches = false;
711
+ break;
712
+ }
713
+ }
714
+ }
715
+ if (matches && schema.then) {
716
+ fields.push(...this._extractWritableFields(schema.then, data, ctx));
717
+ } else if (!matches && schema.else) {
718
+ fields.push(...this._extractWritableFields(schema.else, data, ctx));
719
+ }
720
+ }
721
+
722
+ if (schema.properties) {
723
+ fields.push(...Object.entries(schema.properties)
724
+ // eslint-disable-next-line no-unused-vars
725
+ .filter(([_, def]) => def.readOnly !== true && def.const === undefined)
726
+ .map(([key]) => key));
727
+ }
728
+
729
+ return [...new Set(fields)];
730
+ }
731
+
732
+ /**
733
+ * Construit un brouillon et un proxy à partir des données et du schéma.
734
+ *
735
+ * @param {Object} options - Options de construction.
736
+ * @param {Object} options.data - Données à utiliser pour le brouillon.
737
+ * @param {Object} [options.serverData=null] - Données du serveur.
738
+ * @param {string|Array} options.constant - Nom de la constante ou tableau de constantes.
739
+ * @param {ApiClient} options.apiClient - Instance de l'API.
740
+ * @param {Object} [options.transforms={}] - Transformations à appliquer.
741
+ * @param {boolean} [options.throwOnError=true] - Si vrai, lève une erreur en cas de problème.
742
+ * @param {Array} [options.removeFields=[]] - Liste des champs à ignorer.
743
+ * @returns {Object} - Objet contenant le brouillon et le proxy.
744
+ * @private
745
+ */
746
+ _buildDraftAndProxy({ data = {}, serverData = null, constant, apiClient, transforms = {}, throwOnError = true, removeFields = [] }) {
747
+ const constants = Array.isArray(constant) ? constant : [constant];
748
+ const combinedSchema = {
749
+ allOf: [],
750
+ $defs: {}
751
+ };
752
+
753
+ for (const key of constants) {
754
+ const sch = apiClient.getRequestSchema(key);
755
+ if (!sch) throw new ApiError(`Unable to find schema for ${key}.`);
756
+
757
+ // Extraire et fusionner les $defs
758
+ if (sch.$defs) {
759
+ for (const [defKey, defVal] of Object.entries(sch.$defs)) {
760
+ if (combinedSchema.$defs[defKey]) {
761
+ apiClient._logger.warn(`Duplicate $defs key '${defKey}' from schema '${key}'`);
762
+ } else {
763
+ combinedSchema.$defs[defKey] = defVal;
764
+ }
765
+ }
766
+ }
767
+
768
+ combinedSchema.allOf.push(sch);
769
+ }
770
+ const draft = {};
771
+ let allowed = this._extractWritableFields(combinedSchema, data);
772
+
773
+ if (data.id && allowed.indexOf("id") === -1) {
774
+ allowed.push("id");
775
+ }
776
+ if (data.slug && allowed.indexOf("slug") === -1) {
777
+ allowed.push("slug");
778
+ }
779
+
780
+ allowed = allowed.filter(k => !removeFields.includes(k));
781
+
782
+ for (const key of allowed) {
783
+ const raw = data[key];
784
+ const transformed = typeof transforms[key] === "function" ? transforms[key](raw, data) : raw;
785
+ if (transformed !== undefined) draft[key] = transformed;
786
+ }
787
+
788
+ const proxy = this._createDraftProxy(apiClient, serverData, draft, allowed, transforms, { throwOnError });
789
+
790
+ return { draft, proxy };
791
+ }
792
+
793
+ /**
794
+ * Extrait les champs modifiés du schéma.
795
+ *
796
+ * @param {ApiClient} apiClient - Instance de l'API.
797
+ * @param {string} constant - Nom de la constante.
798
+ * @param {Object} data - Données à comparer.
799
+ * @param {Function} getInitialDraft - Fonction pour obtenir le brouillon initial.
800
+ * @param {Array} removeFields - Liste des champs à ignorer.
801
+ * @returns {Object|null} - Champs modifiés ou null.
802
+ * @private
803
+ */
804
+ _extractChangedFieldsFromSchema(apiClient, constant, data = {}, getInitialDraft, removeFields = []) {
805
+ const schema = apiClient.getRequestSchema(constant);
806
+ let allowed = this._extractWritableFields(schema, data);
807
+ const changed = {};
808
+ const initialDraft = getInitialDraft?.() || {};
809
+
810
+ // on enlève les champs qui ne sont pas dans le draft
811
+ // ou qui sont dans removeFields
812
+ allowed = allowed.filter(k => !removeFields.includes(k));
813
+
814
+ for (const key of allowed) {
815
+ // on verifie que le champ existe dans le draft
816
+ // sinon on ne le prend pas en compte
817
+
818
+ if (data[key] === undefined) continue;
819
+
820
+ const current = data[key];
821
+ const initial = initialDraft[key];
822
+
823
+ const changedValue =
824
+ JSON.stringify(current) !== JSON.stringify(initial);
825
+
826
+ if (changedValue) {
827
+ changed[key] = current;
828
+ }
829
+ }
830
+
831
+ return Object.keys(changed).length > 0 ? changed : null;
832
+ }
833
+
834
+ /**
835
+ * ───────────────────────────────
836
+ * MutualEntityMixin
837
+ * ───────────────────────────────
838
+ */
839
+
840
+ /**
841
+ * Résout l'identifiant de l'entité si seul le slug est fourni.
842
+ * @param {string} type - Le type d'entité (ex : "citoyens", "organizations", "projects").
843
+ * @returns {Promise<string>} L'identifiant résolu.
844
+ * @private
845
+ */
846
+ async _resolveId(type) {
847
+ if (!this.id && this.slug) {
848
+ try {
849
+ const data = await this.endpointApi.getElementsKey({
850
+ pathParams:{
851
+ slug: this.slug
852
+ }
853
+ });
854
+ if(data?.contextId && data?.contextType === type) {
855
+ this._id(data.contextId);
856
+ } else {
857
+ throw new ApiResponseError(`Le slug ${this.slug} ne correspond pas à un ${type}`, 200, data);
858
+ }
859
+ } catch (error) {
860
+ if(error instanceof ApiResponseError) {
861
+ if(error?.responseData?.contextType !== type) {
862
+ throw error;
863
+ } else {
864
+ throw new ApiResponseError(`Impossible de récupérer l'identifiant pour le slug ${this.slug}`, error.status, error.responseData);
865
+ }
866
+ } else {
867
+ throw error;
868
+ }
869
+ }
870
+ }
871
+ return this.id;
872
+ }
873
+
874
+ /**
875
+ * Récupère le profil public de l'entité.
876
+ *
877
+ * @returns {Promise<Object>} - Les données du profil public.
878
+ * @private
879
+ */
880
+ async _getPublicProfile() {
881
+ await this._resolveId(this.getEntityType());
882
+ return this.endpointApi.getElementsAbout({ pathParams: { id: this.id, type: this.getEntityType() } });
883
+ }
884
+
885
+ /**
886
+ * Récupère la classe d'entité et ses dépendances à partir du type d'entité.
887
+ *
888
+ * @param {string} entityType - Le type d'entité.
889
+ * @return {{ entityClass: Function, deps: Object } | null}
890
+ * @private
891
+ */
892
+ _getEntityMeta(entityType) {
893
+ const selfClass = this.constructor;
894
+ const selfTag = this.__entityTag;
895
+
896
+ const commonDeps = {
897
+ EndpointApi: this.deps.EndpointApi,
898
+ User: selfTag === "User" ? selfClass : this.deps.User,
899
+ Organization: selfTag === "Organization" ? selfClass : this.deps.Organization,
900
+ Project: selfTag === "Project" ? selfClass : this.deps.Project,
901
+ Event: selfTag === "Event" ? selfClass : this.deps.Event,
902
+ Poi: selfTag === "Poi" ? selfClass : this.deps.Poi,
903
+ Badge: selfTag === "Badge" ? selfClass : this.deps.Badge,
904
+ News: selfTag === "News" ? selfClass : this.deps.News
905
+ };
906
+
907
+ const map = {
908
+ citoyens: { entityClass: commonDeps.User, deps: commonDeps },
909
+ organizations:{ entityClass: commonDeps.Organization, deps: commonDeps },
910
+ projects: { entityClass: commonDeps.Project, deps: commonDeps },
911
+ events: { entityClass: commonDeps.Event, deps: { ...commonDeps, Badge: undefined } },
912
+ poi: { entityClass: commonDeps.Poi, deps: { ...commonDeps, Badge: undefined, News: undefined } },
913
+ news: { entityClass: commonDeps.News, deps: { ...commonDeps } },
914
+ badges: { entityClass: commonDeps.Badge, deps: {
915
+ EndpointApi: commonDeps.EndpointApi,
916
+ User: commonDeps.User,
917
+ Organization: commonDeps.Organization,
918
+ Project: commonDeps.Project
919
+ } }
920
+ };
921
+
922
+ return map[entityType] || null;
923
+ }
924
+
925
+ /**
926
+ * Lier des données d'entité à une instance d'entité.
927
+ *
928
+ * @param {string} entityType - Le type d'entité (ex : "citoyens", "organisations", "projets").
929
+ * @param {Object} entityData - Les données de l'entité à lier.
930
+ * @return {Object} L'entité liée.
931
+ * @private
932
+ */
933
+ _linkEntity(entityType, entityData) {
934
+ const meta = this._getEntityMeta(entityType);
935
+ if (!meta) return entityData;
936
+ return meta.entityClass.fromServerData(entityData, this, meta.deps);
937
+ }
938
+
939
+ /**
940
+ * Récupère et lie une entité à partir de son ID.
941
+ *
942
+ * @param {string} entityType - Le type d'entité.
943
+ * @param {string} entityId - L'identifiant de l'entité.
944
+ * @param {Object} [options] - Options supplémentaires :
945
+ * @param {boolean} [options.skipGet] - Si true, ne pas appeler `get()` sur l'entité.
946
+ * @return {Promise<Object|null>} L'entité liée ou null.
947
+ * @private
948
+ */
949
+ async _linkEntityById(entityType, entityId, options = {}) {
950
+ const meta = this._getEntityMeta(entityType);
951
+ if (!meta) return null;
952
+ const entity = new meta.entityClass(this, { id: entityId }, meta.deps);
953
+ if(options?.skipGet) return entity;
954
+ await entity.get();
955
+ return entity;
956
+ }
957
+
958
+ /**
959
+ * Lie une liste d'entités à partir de leurs données.
960
+ *
961
+ * @param {Array<Object>} results - Liste de données d'entités.
962
+ * @return {Array<Object>} Liste d'entités liées.
963
+ * @private
964
+ */
965
+ _linkEntities(results) {
966
+ return results.map(d => this._linkEntity?.(d.collection, d) ?? d);
967
+ }
968
+
969
+ /**
970
+ * Lie des entités présentes dans `this.serverData` à partir de leurs IDs,
971
+ * en les filtrant dynamiquement et en optionnellement les transformant.
972
+ *
973
+ * @param {string} entityType - Le type d'entité (ex : "badges", "citoyens", etc.).
974
+ * @param {Object} filters - Clés/valeurs de filtres dynamiques. Les valeurs peuvent être :
975
+ * - un littéral (comparaison stricte ou intelligente selon le type),
976
+ * - une chaîne (utilise `includes` insensible à la casse),
977
+ * - une RegExp (appliquée si la valeur est une chaîne),
978
+ * - une fonction `(value) => boolean`.
979
+ * @param {Object} [options] - Options supplémentaires :
980
+ * @param {string} [options.key] - Le champ de `this.serverData` à utiliser (par défaut = entityType).
981
+ * @param {Function} [options.mapFn] - Fonction de transformation `(entity) => any` appliquée au résultat.
982
+ *
983
+ * @return {Promise<Array<Object>>} Liste des entités liées, filtrées et éventuellement transformées.
984
+ *
985
+ * @example
986
+ * // Tous les badges avec `name` contenant "codev"
987
+ * const badges = await this.linkEntitiesFromServerData("badges", { name: "codev" });
988
+ *
989
+ * @example
990
+ * // Badges non expirés et visibles
991
+ * const badges = await this.linkEntitiesFromServerData("badges", {
992
+ * expiredOn: false,
993
+ * show: "true"
994
+ * });
995
+ *
996
+ * @example
997
+ * // Badges émis après 2023
998
+ * const badges = await this.linkEntitiesFromServerData("badges", {
999
+ * issuedOn: (v) => new Date(v) >= new Date("2023-01-01")
1000
+ * });
1001
+ *
1002
+ * @example
1003
+ * // Extraire uniquement les noms des badges
1004
+ * const namesOnly = await this.linkEntitiesFromServerData("badges", {}, {
1005
+ * mapFn: (badge) => badge.meta.name
1006
+ * });
1007
+ */
1008
+ async linkEntitiesFromServerData(entityType, filters = {}, options = {}) {
1009
+ const key = options.key || entityType;
1010
+ const mapFn = typeof options.mapFn === "function" ? options.mapFn : null;
1011
+
1012
+ const isTruthy = (v) => v === true || v === "true";
1013
+ const isFalsy = (v) => v === false || v === "false";
1014
+
1015
+ const data = this.serverData?.[key];
1016
+ const result = [];
1017
+
1018
+ if (data && typeof data === "object" && Object.keys(data).length > 0) {
1019
+ for (const id of Object.keys(data)) {
1020
+ const meta = data[id];
1021
+
1022
+ const matches = Object.entries(filters).every(([key, expected]) => {
1023
+ const actual = meta[key];
1024
+
1025
+ if (typeof expected === "function") {
1026
+ try {
1027
+ return expected(actual);
1028
+ } catch (err) {
1029
+ console.warn(`Erreur dans le filtre personnalisé pour ${key}`, err);
1030
+ return false;
1031
+ }
1032
+ }
1033
+
1034
+ if (expected instanceof RegExp) {
1035
+ return typeof actual === "string" && expected.test(actual);
1036
+ }
1037
+
1038
+ if (typeof expected === "string" && typeof actual === "string") {
1039
+ return actual.toLowerCase().includes(expected.toLowerCase());
1040
+ }
1041
+
1042
+ if (isTruthy(expected)) return isTruthy(actual);
1043
+ if (isFalsy(expected)) return isFalsy(actual);
1044
+
1045
+ if (
1046
+ typeof actual === "string" &&
1047
+ typeof expected === "string" &&
1048
+ !isNaN(Date.parse(actual)) &&
1049
+ !isNaN(Date.parse(expected))
1050
+ ) {
1051
+ return new Date(actual).getTime() === new Date(expected).getTime();
1052
+ }
1053
+
1054
+ return actual === expected;
1055
+ });
1056
+
1057
+ if (!matches) continue;
1058
+
1059
+ try {
1060
+ const entity = await this._linkEntityById(entityType, id);
1061
+ if (entity) {
1062
+ entity.meta = meta;
1063
+ result.push(mapFn ? mapFn(entity) : entity);
1064
+ }
1065
+ } catch (error) {
1066
+ this._logger?.error?.(`Erreur lors de la récupération de l'entité ${entityType} (${id}) :`, error);
1067
+ }
1068
+ }
1069
+ }
1070
+ return result;
1071
+ }
1072
+
1073
+ /**
1074
+ * Crée une instance d'entité à partir des données fournies.
1075
+ * @param {string} entityType - Le type d'entité (ex : "citoyens", "organisations", "projets").
1076
+ * @param {Object} entityData - Les données de l'entité.
1077
+ * @return {Object} L'entité créée.
1078
+ * @private
1079
+ */
1080
+ _entityInstanceData(entityType, entityData) {
1081
+ const meta = this._getEntityMeta(entityType);
1082
+ const selfKey = this.__entityTag; // ex: "User", "Organization", etc.
1083
+ const selfClass = this.constructor;
1084
+
1085
+ // pour citoyens la signature est différentes
1086
+ return new meta.entityClass(this, entityData, {
1087
+ [selfKey]: selfClass,
1088
+ ...meta.deps
1089
+ });
1090
+ }
1091
+
1092
+ /**
1093
+ * Crée une instance d'entité, et déclenche get() si certaines propriétés sont présentes.
1094
+ * @param {string} entityType
1095
+ * @param {Object} entityData
1096
+ * @return {Promise<Object>}
1097
+ */
1098
+ async entity(entityType, entityData = {}) {
1099
+ try {
1100
+ const entity = this._entityInstanceData(entityType, entityData);
1101
+
1102
+ const fetchKeysByEntity = {
1103
+ citoyens: ["id", "slug"],
1104
+ organisations: ["id", "slug"],
1105
+ projets: ["id", "slug"],
1106
+ events: ["id", "slug"],
1107
+ poi: ["id", "slug"],
1108
+ news: ["id"],
1109
+ badges: ["id"],
1110
+ };
1111
+
1112
+ const fetchKeys = fetchKeysByEntity[entityType];
1113
+
1114
+ if (fetchKeys && this._hasAtLeastOne(entityData, fetchKeys)) {
1115
+ await entity.get();
1116
+ }
1117
+
1118
+ return entity;
1119
+ } catch (error) {
1120
+ this.apiClient._logger.error(`[Api.${this.__entityTag}.${entityType}] Erreur lors de la création d'une instance ${entityType} :`, error.message);
1121
+ throw error;
1122
+ }
1123
+ }
1124
+
1125
+ /**
1126
+ * ───────────────────────────────
1127
+ * EntityMixin
1128
+ * ───────────────────────────────
1129
+ */
1130
+
1131
+ /**
1132
+ * Récupérer le profil public de l'entité
1133
+ *
1134
+ * @returns {Promise<Object>} - Les données de réponse.
1135
+ */
1136
+ async get() {
1137
+ return this.apiClient.safeCall(async () => {
1138
+ const data = await this._getPublicProfile();
1139
+ this._setData(data);
1140
+ return data;
1141
+ });
1142
+ }
1143
+
1144
+ /**
1145
+ * Mettre à jour les paramètres d'un élément : Mise à jour des paramètres spécifiques d'un élément.
1146
+ * Constant : UPDATE_SETTINGS
1147
+ * @param {Object} data - Les données à envoyer.
1148
+ * @param {string} data.type - data.type
1149
+ * @param {undefined} data.value - data.value
1150
+ * @returns {Promise<Object>} - Les données de réponse.
1151
+ * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
1152
+ * @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
1153
+ * @throws {Error} - En cas d'erreur inattendue.
1154
+ */
1155
+ async updateSettings(data = {}) {
1156
+ data.idEntity = this.id;
1157
+ data.typeEntity = this.getEntityType();
1158
+ return this.callIsConnected(() => this.endpointApi.updateSettings(data));
1159
+ }
1160
+
1161
+ /**
1162
+ * Mettre à jour la description d'un élément : Permet de mettre à jour la description courte et complète d'un élément.
1163
+ * Constant : UPDATE_BLOCK_DESCRIPTION
1164
+ * @param {Object} data - Les données à envoyer.
1165
+ * @param {string} data.descMentions - Mentions dans la description (default: "")
1166
+ * @param {string} data.shortDescription - Courte description
1167
+ * @param {string} data.description - Description complète
1168
+ * @returns {Promise<Object>} - Les données de réponse.
1169
+ * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
1170
+ * @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
1171
+ * @throws {Error} - En cas d'erreur inattendue.
1172
+ */
1173
+ async updateDescription(data = {}) {
1174
+ data.typeElement = this.getEntityType();
1175
+ data.id = this.id;
1176
+ return this.callIsConnected(() => this.endpointApi.updateBlockDescription(data));
1177
+ }
1178
+
1179
+ /**
1180
+ * Mettre à jour les informations d'un élément : Permet de mettre à jour les informations générales d'un élément (nom, contacts, etc.).
1181
+ * Constant : UPDATE_BLOCK_INFO
1182
+ */
1183
+ async updateInfo(data = {}) {
1184
+ data.typeElement = this.getEntityType();
1185
+ data.id = this.id;
1186
+ return this.callIsConnected(() => this.endpointApi.updateBlockInfo(data));
1187
+ }
1188
+
1189
+ /**
1190
+ * Mettre à jour les réseaux sociaux d'un élément : Permet de mettre à jour les liens vers les réseaux sociaux d'un élément.
1191
+ * Constant : UPDATE_BLOCK_SOCIAL
1192
+ */
1193
+ async updateSocial(data = {}) {
1194
+ data.typeElement = this.getEntityType();
1195
+ data.id = this.id;
1196
+ return this.callIsConnected(() => this.endpointApi.updateBlockSocial(data));
1197
+ }
1198
+
1199
+ /**
1200
+ * Mettre à jour les localités d'un élément : Permet de mettre à jour l'adresse et les informations géographiques d'un élément.
1201
+ * Constant : UPDATE_BLOCK_LOCALITY
1202
+ */
1203
+ async updateLocality(data = {}) {
1204
+ data.typeElement = this.getEntityType();
1205
+ data.id = this.id;
1206
+ return this.callIsConnected(() => this.endpointApi.updateBlockLocality(data));
1207
+ }
1208
+
1209
+ /**
1210
+ * Mettre à jour le slug d'un élément : Permet de mettre à jour le slug pour une URL simplifiée.
1211
+ * Constant : UPDATE_BLOCK_SLUG
1212
+ * @param {Object} data - Les données à envoyer.
1213
+ * @param {string} data.slug - Le nouveau slug à appliquer.
1214
+ * @returns {Promise<Object>} - Les données de réponse.
1215
+ */
1216
+ async updateSlug({ slug }) {
1217
+ try {
1218
+ await this.endpointApi.check({ type: this.getEntityType(), id: this.id, slug });
1219
+ } catch (error) {
1220
+ if(error instanceof ApiResponseError) {
1221
+ throw new ApiResponseError("Erreur lors de la vérification du slug.", error.status, error.data);
1222
+ }
1223
+ throw error;
1224
+ }
1225
+ return this.callIsConnected(() => this.endpointApi.updateBlockSlug({ typeElement: this.getEntityType(), id: this.id, slug }));
1226
+ }
1227
+
1228
+ /**
1229
+ * Mettre à jour l'image de profil : Permet de mettre à jour l'image de profil d'un utilisateur ou d'une entité.
1230
+ * Constant : PROFIL_IMAGE
1231
+ * @param {Object} data - Les données à envoyer.
1232
+ * @param {string} data.profil_avatar - L'image de profil à mettre à jour.
1233
+ * @returns {Promise<Object>} - Les données de réponse.
1234
+ */
1235
+ async updateImageProfil({ profil_avatar: image }) {
1236
+ image = await this._validateImage(image);
1237
+ const data = { pathParams: { folder: this.getEntityType(), ownerId: this.id }, profil_avatar: image };
1238
+ return this.callIsConnected(() => this.endpointApi.profilImage(data));
1239
+ }
1240
+
1241
+ /**
1242
+ * Crée une instance d'organisation et récupère son profil si nécessaire.
1243
+ *
1244
+ * @param {Object} organizationData - Les données nécessaires pour initialiser l'organisation.
1245
+ * @returns {Promise<Organization>} Une promesse qui résout l'objet Organisation créé.
1246
+ * @throws {Error} Si une erreur se produit lors de la création de l'organisation.
1247
+ */
1248
+ async organization(organizationData = {}) {
1249
+ if(!this.isMe){
1250
+ throw new ApiError("Vous devez être connecté et être l'utilisateur pour créer une organisation.");
1251
+ }
1252
+ return this.entity("organizations", organizationData);
1253
+ }
1254
+
1255
+ /**
1256
+ * Crée une instance de projet et récupère son profil si nécessaire.
1257
+ *
1258
+ * @param {Object} projectData - Les données nécessaires pour initialiser le projet.
1259
+ * @returns {Promise<Project>} Une promesse qui résout l'objet Projet créé.
1260
+ * @throws {Error} Si une erreur se produit lors de la création du projet.
1261
+ */
1262
+ async project(projectData = {}) {
1263
+ // TODO: Vérifier si l'utilisateur est admin de l'organisation
1264
+ if(!this.isConnected){
1265
+ throw new ApiError("Vous devez être connecté pour créer un projet.");
1266
+ }
1267
+ return this.entity("projects", projectData);
1268
+ }
1269
+
1270
+ /**
1271
+ * Crée une instance de POI et la récupère si nécessaire.
1272
+ *
1273
+ * @param {Object} poiData - Les données nécessaires pour initialiser le POI.
1274
+ * @returns {Promise<Poi>} Une promesse qui résout l'objet POI créé.
1275
+ * @throws {Error} Si une erreur se produit lors de la création du POI.
1276
+ */
1277
+ async poi(poiData = {}) {
1278
+ // TODO: Vérifier si l'utilisateur est admin de l'organisation
1279
+ if(!this.isConnected){
1280
+ throw new ApiError("Vous devez être connecté pour créer un POI.");
1281
+ }
1282
+ return this.entity("poi", poiData);
1283
+ }
1284
+
1285
+ /**
1286
+ * Crée une instance d'événement et la récupère si nécessaire.
1287
+ *
1288
+ * @param {Object} eventData - Les données nécessaires pour initialiser l'événement.
1289
+ * @returns {Promise<Event>} Une promesse qui résout l'objet Événement créé.
1290
+ * @throws {Error} Si une erreur se produit lors de la création de l'événement.
1291
+ */
1292
+ async event(eventData = {}) {
1293
+ // TODO: Vérifier si l'utilisateur est admin de l'organisation
1294
+ if(!this.isConnected){
1295
+ throw new ApiError("Vous devez être connecté pour créer un événement.");
1296
+ }
1297
+ return this.entity("events", eventData);
1298
+ }
1299
+
1300
+ /**
1301
+ * Crée une instance de badge et la récupère si nécessaire.
1302
+ *
1303
+ * @param {Object} badgeData - Les données nécessaires pour initialiser le badge.
1304
+ * @returns {Promise<Badge>} Une promesse qui résout l'objet Badge créé.
1305
+ * @throws {Error} Si une erreur se produit lors de la création du badge.
1306
+ */
1307
+ async badge(badgeData = {}) {
1308
+ // TODO: Vérifier si l'utilisateur est admin de l'organisation
1309
+ if(!this.isConnected){
1310
+ throw new ApiError("Vous devez être connecté pour créer un badge.");
1311
+ }
1312
+ return this.entity("badges", badgeData);
1313
+ }
1314
+
1315
+ /**
1316
+ * Crée une instance de news et la récupère si nécessaire.
1317
+ *
1318
+ * @param {Object} newsData - Les données nécessaires pour initialiser la news.
1319
+ * @returns {Promise<News>} Une promesse qui résout l'objet News créé.
1320
+ * @throws {Error} Si une erreur se produit lors de la création de la news.
1321
+ */
1322
+ async news(newsData = {}) {
1323
+ if(!this.isConnected){
1324
+ throw new ApiError("Vous devez être connecté.");
1325
+ }
1326
+ return this.entity("news", newsData);
1327
+ }
1328
+
1329
+ /**
1330
+ * Récupérer les organisations d'une entitée : la liste des organisations dont l'entité est membre ou admin valide.
1331
+ * Constant : GET_ORGANIZATIONS_ADMIN | GET_ORGANIZATIONS_NO_ADMIN
1332
+ * @param {Object} data - Les données à envoyer.
1333
+ * @returns {Promise<Object>} - Les données de réponse.
1334
+ */
1335
+ async getOrganizations(data = {}) {
1336
+
1337
+ if(this.isMe){
1338
+ data.pathParams = { type: this.getEntityType(), id: this.id };
1339
+ // NOTE : dans le schema je crois que si pas de data.filters alors le default ce fait avec data.pathParams
1340
+ // data.filters = {
1341
+ // [`links.members.${this.id}`]: { "$exists": true },
1342
+ // [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
1343
+ // [`links.members.${this.id}.isInviting`]: { "$exists": false }
1344
+ // };
1345
+ } else {
1346
+ delete data?.pathParams;
1347
+ data.filters = {
1348
+ [`links.members.${this.id}`]: { "$exists": true },
1349
+ [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
1350
+ [`links.members.${this.id}.isInviting`]: { "$exists": false }
1351
+ };
1352
+ }
1353
+
1354
+ const fetchFn = this.isMe
1355
+ ? () => this.callIsMe(() => this.endpointApi.getOrganizationsAdmin(data))
1356
+ : () => this.endpointApi.getOrganizationsNoAdmin(data);
1357
+
1358
+ const arrayObjet = await fetchFn();
1359
+
1360
+ if (!Array.isArray(arrayObjet.results)) {
1361
+ throw new ApiResponseError("Erreur lors de la récupération des organisations.", 500, arrayObjet.results);
1362
+ }
1363
+
1364
+ // nettoyage du count
1365
+ delete arrayObjet?.count?.spam;
1366
+
1367
+ // calcul du total
1368
+ const totalCount = Object.values(arrayObjet.count || {}).reduce((acc, val) => acc + val, 0);
1369
+ arrayObjet.count.total = totalCount;
1370
+
1371
+ const rawList = this._linkEntities(arrayObjet.results);
1372
+
1373
+ return {
1374
+ count: arrayObjet.count,
1375
+ results: rawList
1376
+ };
1377
+ }
1378
+
1379
+ /**
1380
+ * Récupérer les projets d'une entitée : liste des projets de l'entité ou elle est "parent" ou "contributeur".
1381
+ * Constant : GET_PROJECTS_ADMIN | GET_PROJECTS_NO_ADMIN
1382
+ * @param {Object} data - Les données à envoyer.
1383
+ * @returns {Promise<Object>} - Les données de réponse.
1384
+ */
1385
+ async getProjects(data = {}) {
1386
+
1387
+ if(this.isMe){
1388
+ data.pathParams = { type: this.getEntityType(), id: this.id };
1389
+ // NOTE : dans le schema je crois que si pas de data.filters alors le default ce fait avec data.pathParams
1390
+ // data.filters = {
1391
+ // "$or": {
1392
+ // [`links.contributors.${this.id}`]: { "$exists": true },
1393
+ // [`parent.${this.id}`]: { "$exists": true }
1394
+ // },
1395
+ // [`links.contributors.${this.id}`]: { "$exists": true }
1396
+ // };
1397
+ } else {
1398
+ delete data?.pathParams;
1399
+ data.filters = {
1400
+ "$or": {
1401
+ [`links.contributors.${this.id}`]: { "$exists": true },
1402
+ [`parent.${this.id}`]: { "$exists": true }
1403
+ },
1404
+ [`links.contributors.${this.id}`]: { "$exists": true }
1405
+ };
1406
+ }
1407
+
1408
+ const fetchFn = this.isMe
1409
+ ? () => this.callIsMe(() => this.endpointApi.getProjectsAdmin(data))
1410
+ : () => this.endpointApi.getProjectsNoAdmin(data);
1411
+
1412
+
1413
+ const arrayObjet = await fetchFn();
1414
+
1415
+ if (!Array.isArray(arrayObjet.results)) {
1416
+ throw new ApiResponseError("Erreur lors de la récupération des projets.", 500, arrayObjet.results);
1417
+ }
1418
+
1419
+ const rawList = this._linkEntities(arrayObjet.results);
1420
+
1421
+ return {
1422
+ count: arrayObjet?.count?.projects ?? 0,
1423
+ results: rawList
1424
+ };
1425
+ }
1426
+
1427
+ /**
1428
+ * Récupérer les POIs d'une entité : liste des POIs de l'entité ou elle est "parent".
1429
+ * Constant : GET_POIS_NO_ADMIN / GET_POIS_ADMIN
1430
+ * @param {Object} data - Les données à envoyer.
1431
+ * @returns {Promise<Object>} - Les données de réponse.
1432
+ */
1433
+ async getPois(data = {}) {
1434
+
1435
+ if(this.isMe){
1436
+ data.pathParams = { type: this.getEntityType(), id: this.id };
1437
+ // NOTE : dans le schema je crois que si pas de data.filters alors le default ce fait avec data.pathParams
1438
+ // data.filters = {
1439
+ // [`parent.${this.id}`]: { "$exists": true },
1440
+ // };
1441
+ } else {
1442
+ delete data?.pathParams;
1443
+ data.filters = {
1444
+ [`parent.${this.id}`]: { "$exists": true },
1445
+ };
1446
+ }
1447
+
1448
+ const fetchFn = this.isMe
1449
+ ? () => this.callIsMe(() => this.endpointApi.getPoisAdmin(data))
1450
+ : () => this.endpointApi.getPoisNoAdmin(data);
1451
+
1452
+ const arrayObjet = await fetchFn();
1453
+ if (!Array.isArray(arrayObjet.results)) {
1454
+ throw new ApiResponseError("Erreur lors de la récupération des POIs.", 500, arrayObjet.results);
1455
+ }
1456
+
1457
+ // lier les entités au objets
1458
+ const rawList = this._linkEntities(arrayObjet.results);
1459
+
1460
+ return {
1461
+ count: arrayObjet.count?.poi ?? 0,
1462
+ results: rawList
1463
+ };
1464
+ }
1465
+
1466
+ /**
1467
+ * Récupérer les abonnés d'une entité
1468
+ * Constant : GET_SUBSCRIBERS
1469
+ * @param {Object} data - Les données à envoyer.
1470
+ * @returns {Promise<Object>} - Les données de réponse.
1471
+ */
1472
+ async getSubscribers(data = {}) {
1473
+ delete data?.pathParams;
1474
+
1475
+ data.filters = {
1476
+ [`links.follows.${this.id}`]: { "$exists": true },
1477
+ [`links.follows.${this.id}.toBeValidated`]: { "$exists": false },
1478
+ [`links.follows.${this.id}.isInviting`]: { "$exists": false }
1479
+ };
1480
+
1481
+ const arrayObjet = await this.endpointApi.getSubscribers(data);
1482
+ if (!Array.isArray(arrayObjet.results)) {
1483
+ throw new ApiResponseError("Erreur lors de la récupération des abonnés.", 500, arrayObjet.results);
1484
+ }
1485
+
1486
+ // lier les entités au objets
1487
+ const rawList = this._linkEntities(arrayObjet.results);
1488
+
1489
+ return {
1490
+ count: arrayObjet.count,
1491
+ results: rawList
1492
+ };
1493
+ }
1494
+
1495
+ /**
1496
+ * Liste des badges créés par l'entité
1497
+ * Constant : GET_BADGES
1498
+ * @param {Object} data - Les données à envoyer.
1499
+ * @returns {Promise<Object>} - Les données de réponse.
1500
+ */
1501
+ async getBadgesIssuer(data = {}) {
1502
+ delete data?.pathParams;
1503
+
1504
+ data.filters = data.filters || {};
1505
+ data.filters["$or"] = {};
1506
+ data.filters["$or"][`issuer.${this.id}`] = { "$exists": true };
1507
+
1508
+ const arrayObjet = await this.endpointApi.getBadges(data);
1509
+ if (!Array.isArray(arrayObjet.results)) {
1510
+ throw new ApiResponseError("Erreur lors de la récupération des badges.", 500, arrayObjet.results);
1511
+ }
1512
+
1513
+ // lier les entités au objets
1514
+ const rawList = this._linkEntities(arrayObjet.results);
1515
+
1516
+ return {
1517
+ count: arrayObjet.count?.badges ?? 0,
1518
+ results: rawList
1519
+ };
1520
+ }
1521
+
1522
+ /**
1523
+ * Récupérer les actualités : Récupère la liste d’actualités selon plusieurs critères.
1524
+ * Constant : GET_NEWS
1525
+ * @param {Object} data - Les données à envoyer.
1526
+ * @param {number} data.dateLimit - Limite de date timestamp ou 0 (default: 0)
1527
+ * @param {object} data.search - data.search
1528
+ * @param {string} data.search.name - Nom ou terme recherché (default: "")
1529
+ * @param {number} data.indexStep - Nombre de résultats par page (default: 12)
1530
+ * @returns {Promise<Object>} - Les données de réponse.
1531
+ * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
1532
+ * @throws {Error} - En cas d'erreur inattendue.
1533
+ */
1534
+ async getNews(data = {}) {
1535
+ data.pathParams = { type: this.getEntityType(), id: this.id };
1536
+ const arrayObjet = await this.endpointApi.getNews(data);
1537
+ if(!Array.isArray(arrayObjet)){
1538
+ throw new ApiResponseError("Erreur lors de la récupération des actualités.", 500, arrayObjet);
1539
+ }
1540
+
1541
+ const rawList = this._linkEntities(arrayObjet);
1542
+
1543
+ return this._createFilteredProxy(rawList);
1544
+ }
1545
+
1546
+ }
170
1547
 
171
1548
  export default BaseEntity;