@communecter/cocolight-api-client 1.0.9 → 1.0.11

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/src/api/User.js CHANGED
@@ -1,34 +1,67 @@
1
- import { ApiResponseError } from "../error.js";
2
- import { News } from "./News.js";
3
- import { EntityMixin } from "../mixin/EntityMixin.js";
4
- import { NewsMixin } from "../mixin/NewsMixin.js";
1
+ import { ApiError, ApiResponseError } from "../error.js";
2
+ import { DraftStateMixin } from "../mixin/DraftStateMixin.js";
3
+ import { MutualEntityMixin } from "../mixin/MutualEntityMixin.js";
5
4
  import { UserMixin } from "../mixin/UserMixin.js";
6
5
  import { UtilMixin } from "../mixin/UtilMixin.js";
7
6
 
8
7
  // User.js
9
8
  export class User {
10
- // Champs privés pour protéger l'état
11
- #id;
12
- #slug;
13
- #data;
9
+ #draftData = {};
10
+ #initialDraftData = {};
11
+ #serverData = null;
12
+
13
+ static entityType = "citoyens";
14
+
15
+ static SCHEMA_CONSTANTS = [
16
+ "UPDATE_BLOCK_DESCRIPTION",
17
+ "UPDATE_BLOCK_INFO",
18
+ "UPDATE_BLOCK_SOCIAL",
19
+ "UPDATE_BLOCK_LOCALITY",
20
+ "UPDATE_BLOCK_SLUG",
21
+ "PROFIL_IMAGE"
22
+ ];
23
+
24
+ static UPDATE_BLOCKS = new Map([
25
+ ["UPDATE_BLOCK_DESCRIPTION", "updateDescription"],
26
+ ["UPDATE_BLOCK_SOCIAL", "updateSocial"],
27
+ ["UPDATE_BLOCK_LOCALITY", "updateLocality"],
28
+ ["UPDATE_BLOCK_INFO", "updateInfo"],
29
+ ["UPDATE_BLOCK_SLUG", "updateSlug"],
30
+ ["PROFIL_IMAGE", "updateImageProfil"]
31
+ ]);
14
32
 
15
33
  /**
16
34
  * Crée une instance de User.
17
- * @param {ApiClient} apiClient - L'instance d'ApiClient.
18
- * @param {Object} identifier - Objet contenant { id } ou { slug }.
19
- * @param {Object} [data={}] - Données supplémentaires.
20
- * @param {Object} [deps={}] - Dépendances injectées (ex: User).
21
- * @param {EndpointApi} deps.EndpointApi - Classe EndpointApi pour éviter les dépendances circulaires.
35
+ *
36
+ * @param {ApiClient} apiClient - Le client API connecté.
37
+ * @param {Object} data - Données initiales (peuvent inclure `id`, `slug`, etc.).
38
+ * @param {string} [data.id] - ID de l'utilisateur.
39
+ * @param {string} [data.slug] - Slug de l'utilisateur.
40
+ * @param {Object} deps - Dépendances injectées.
41
+ * @param {function|object} deps.EndpointApi - Classe ou instance de EndpointApi.
42
+ * @param {function} deps.Organization - Classe Organization.
43
+ * @param {function} deps.Project - Classe Project.
44
+ * @param {function} deps.News - Classe News.
45
+ *
46
+ * @throws {ApiError} - Si des dépendances nécessaires sont manquantes ou invalides.
22
47
  */
23
48
 
24
- constructor(apiClient, { id, slug } = {}, data = {}, deps = {}) {
49
+ constructor(apiClient, data = {}, deps = {}) {
50
+ this.__entityTag = "User";
51
+
25
52
  if(!deps.EndpointApi){
26
- throw new Error("EndpointApi class must be injected to avoid circular dependency.");
53
+ throw new ApiError("EndpointApi class must be injected to avoid circular dependency.");
27
54
  }
28
- if (!id && !slug) {
29
- throw new Error("Vous devez fournir un id ou un slug pour créer un User.");
55
+ if (!data?.id && !data?.slug) {
56
+ throw new ApiError("Vous devez fournir un id ou un slug pour créer un User.");
30
57
  }
58
+
59
+ if (!deps.Organization) throw new ApiError("Organization class must be injected.");
60
+ if (!deps.Project) throw new ApiError("Project class must be injected.");
61
+ if (!deps.News) throw new ApiError("News class must be injected.");
62
+
31
63
  this.apiClient = apiClient;
64
+
32
65
  this.deps = deps;
33
66
 
34
67
  // Gérer les deux cas : fonction constructeur ou instance
@@ -37,47 +70,59 @@ export class User {
37
70
  } else if (typeof deps.EndpointApi === "object") {
38
71
  this.endpointApi = deps.EndpointApi;
39
72
  } else {
40
- throw new Error("deps.EndpointApi doit être une classe ou une instance valide.");
73
+ throw new ApiError("deps.EndpointApi doit être une classe ou une instance valide.");
41
74
  }
42
-
43
- this.#id = id || null;
44
- this.#slug = slug || null;
45
- this.#data = data;
75
+
76
+ this.#serverData = null;
77
+
78
+ const { draft, proxy } = this.buildDraftAndProxy({
79
+ data: { ...data, ...this.defaultFields },
80
+ serverData: this.#serverData,
81
+ constant: User.SCHEMA_CONSTANTS,
82
+ apiClient: this.apiClient,
83
+ transforms: this.transforms,
84
+ removeFields: this.removeFields
85
+ });
86
+
87
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft)); // snapshot propre
88
+ this.#draftData = draft;
89
+ this.data = proxy;
46
90
  }
47
-
48
- // Getters en lecture seule pour chaque propriété
91
+
49
92
  get id() {
50
- return this.#id;
93
+ return this.#draftData.id || null;
51
94
  }
52
95
 
53
96
  _id(newId) {
54
- this.#id = newId;
97
+ this.#draftData.id = newId;
55
98
  }
56
-
99
+
57
100
  get slug() {
58
- return this.#slug;
59
- }
60
-
61
- _slug(newSlug) {
62
- this.#slug = newSlug;
101
+ return this.#draftData.slug || null;
63
102
  }
64
103
 
65
104
  get isConnected() {
66
105
  return this.apiClient.isConnected;
67
106
  }
68
107
 
69
- // Getter pour accéder aux données
70
- get data() {
71
- return this.#data;
72
- }
73
-
74
- // Méthode interne qui permet de mettre à jour les données
75
108
  _setData(newData) {
76
- this.#data = newData;
109
+ this.#serverData = { ...newData };
110
+
111
+ const { draft, proxy } = this.buildDraftAndProxy({
112
+ data: { ...newData, ...this.defaultFields },
113
+ serverData: this.#serverData,
114
+ constant: User.SCHEMA_CONSTANTS,
115
+ apiClient: this.apiClient,
116
+ transforms: this.transforms,
117
+ removeFields: this.removeFields
118
+ });
119
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft));
120
+ this.#draftData = draft;
121
+ this.data = proxy;
77
122
  }
78
123
 
79
124
  getEntityType() {
80
- return "citoyens";
125
+ return User.entityType;
81
126
  }
82
127
 
83
128
  get userId() {
@@ -88,6 +133,23 @@ export class User {
88
133
  return this.isConnected && this.userId === this.id;
89
134
  }
90
135
 
136
+ get draftData() {
137
+ return this.#draftData;
138
+ }
139
+
140
+ get initialDraftData() {
141
+ return this.#initialDraftData;
142
+ }
143
+
144
+ get serverData() {
145
+ return this.#serverData;
146
+ }
147
+
148
+ async refresh() {
149
+ if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.");
150
+ return this.get();
151
+ }
152
+
91
153
  /**
92
154
  * Récupère le profil complet de l'utilisateur.
93
155
  * Si l'utilisateur est connecté, on appelle le endpoint ME_INFO_URL,
@@ -95,13 +157,15 @@ export class User {
95
157
  *
96
158
  * @returns {Promise<Object>} Le profil complet.
97
159
  */
98
- async getProfil() {
160
+ async get() {
99
161
  return this.apiClient.safeCall(async () => {
100
162
  if (this.isMe) {
101
163
  const data = await this.endpointApi.meInfoUrl();
164
+ this._setData(data);
102
165
  return data;
103
166
  } else {
104
167
  const data = await this.getPublicProfile();
168
+ this._setData(data);
105
169
  return data;
106
170
  }
107
171
  });
@@ -123,12 +187,74 @@ export class User {
123
187
  return this.callIsMe(() => this.endpointApi.deleteAccount(data));
124
188
  }
125
189
 
190
+ /**
191
+ * Sauvegarde les modifications de l'utilisateur en appelant les endpoints correspondants.
192
+ * Seul l'utilisateur connecté peut se modifier lui-même.
193
+ *
194
+ * @returns {Promise<Object>} - Données serveur mises à jour si applicable.
195
+ * @throws {ApiError} - Si l'utilisateur n'est pas autorisé.
196
+ */
197
+ async save() {
198
+
199
+ if(!this.isMe){
200
+ throw new ApiError("Vous devez être connecté et être l'utilisateur pour sauvegarder.");
201
+ }
202
+
203
+ const payload = { ...this.#draftData };
204
+
205
+ if (this.id) {
206
+ const hasChanged = await this._update(payload);
207
+ if (hasChanged) {
208
+ // this._updateInitialDraftSnapshot();
209
+ await this.refresh();
210
+ }
211
+ return this.#serverData;
212
+ }
213
+
214
+ }
215
+
216
+ /**
217
+ * Met à jour les blocs modifiés de l'utilisateur via les constantes de schéma définies.
218
+ *
219
+ * @param {Object} payload - Données courantes à comparer et envoyer.
220
+ * @returns {Promise<boolean>} - Indique s'il y a eu une modification réelle.
221
+ */
222
+ async _update(payload){
223
+ if(payload.id){
224
+ delete payload.id;
225
+ }
226
+
227
+ let hasChanged = false;
228
+
229
+ // Sinon, on fait les updates en blocs
230
+ for (const [constant, methodName] of User.UPDATE_BLOCKS) {
231
+ const blockData = this.extractChangedFieldsFromSchema(
232
+ this.apiClient,
233
+ constant,
234
+ { ...payload, ...this.defaultFields},
235
+ () => this.initialDraftData,
236
+ this.removeFields
237
+ );
238
+ if (blockData && Object.keys(blockData).length > 0) {
239
+ await this[methodName](blockData);
240
+ hasChanged = true;
241
+ }
242
+ }
243
+
244
+ return hasChanged;
245
+ }
246
+
126
247
  /**
127
248
  * Mettre à jour les paramètres utilisateur : Mise à jour des paramètres spécifiques d'un utilisateur.
128
249
  * Constant : UPDATE_SETTINGS
250
+ * @param {Object} data - Données à mettre à jour.
251
+ * @param {"birthDate"|"email"|"locality"|"phone"|"directory"} data.type - Type de paramètre à mettre à jour.
252
+ * @param {"private"|"public"|"mask"} data.value - Nouvelle valeur du paramètre.
253
+ * @returns {Promise<void>} - Résultat de la mise à jour.
129
254
  */
130
255
  async updateSettings(data = {}) {
131
- return this.callIsMe(() => this.endpointApi.updateSettings(data));
256
+ await this.callIsMe(() => this.endpointApi.updateSettings(data));
257
+ await this.refresh();
132
258
  }
133
259
 
134
260
  /**
@@ -167,7 +293,7 @@ export class User {
167
293
  * Mettre à jour le slug d'un élément : Permet de mettre à jour le slug pour une URL simplifiée.
168
294
  * Constant : UPDATE_BLOCK_SLUG
169
295
  */
170
- async updateSlug(slug) {
296
+ async updateSlug({ slug }) {
171
297
  try {
172
298
  await this.endpointApi.check({ slug });
173
299
  } catch (error) {
@@ -183,13 +309,143 @@ export class User {
183
309
  * Mettre à jour l'image de profil : Permet de mettre à jour l'image de profil d'un utilisateur ou d'une entité.
184
310
  * Constant : PROFIL_IMAGE
185
311
  */
186
- async updateImageProfil(image) {
312
+ async updateImageProfil({ profil_avatar: image }) {
187
313
  image = await this.validateImage(image);
188
314
  return this.callIsMe(() => this.endpointApi.profilImage({ profil_avatar: image }));
189
315
  }
316
+
317
+ /**
318
+ * Récupérer les organisations d'un utilisateur : Récupère la liste des organisations auxquelles l'utilisateur appartient.
319
+ * Constant : GET_ORGANIZATIONS_ADMIN | GET_ORGANIZATIONS_NO_ADMIN
320
+ */
321
+ async getOrganizations(data = {}) {
322
+ delete data?.pathParams;
323
+
324
+ const fetchFn = this.isMe
325
+ ? () => this.callIsMe(() => this.endpointApi.getOrganizationsAdmin(data))
326
+ : () => this.endpointApi.getOrganizationsNoAdmin(data);
327
+
328
+ if (!this.isMe && !data.filters) {
329
+ data.filters = {
330
+ [`links.members.${this.id}`]: { "$exists": true },
331
+ [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
332
+ [`links.members.${this.id}.isInviting`]: { "$exists": false }
333
+ };
334
+ }
335
+
336
+ const arrayObjetOrganizations = await fetchFn();
337
+
338
+ if (!Array.isArray(arrayObjetOrganizations.results)) {
339
+ throw new ApiResponseError("Erreur lors de la récupération des organisations.", 500, arrayObjetOrganizations.results);
340
+ }
341
+
342
+ // nettoyage du count
343
+ delete arrayObjetOrganizations?.count?.spam;
344
+
345
+ // calcul du total
346
+ const totalCount = Object.values(arrayObjetOrganizations.count || {}).reduce((acc, val) => acc + val, 0);
347
+ arrayObjetOrganizations.count.total = totalCount;
348
+
349
+ // transformation des résultats
350
+ const rawOrganizationsList = arrayObjetOrganizations.results.map((orgData) =>
351
+ this.deps.Organization.fromServerData(orgData, this, {
352
+ User,
353
+ Project: this.deps.Project,
354
+ News: this.deps.News,
355
+ EndpointApi: this.deps.EndpointApi
356
+ })
357
+ );
358
+
359
+ return {
360
+ count: arrayObjetOrganizations.count,
361
+ results: rawOrganizationsList
362
+ };
363
+ }
364
+
365
+ /**
366
+ * Crée une instance d'organisation et récupère son profil si nécessaire.
367
+ *
368
+ * @param {Object} organizationData - Les données nécessaires pour initialiser l'organisation.
369
+ * @returns {Promise<Organization>} Une promesse qui résout l'objet Organisation créé.
370
+ * @throws {Error} Si une erreur se produit lors de la création de l'organisation.
371
+ */
372
+ async organization(organizationData = {}, ) {
373
+ try {
374
+ const organization = new this.deps.Organization(this, organizationData, { User, Project: this.deps.Project, News: this.deps.News, EndpointApi : this.deps.EndpointApi });
375
+ if (organizationData.id || organizationData.slug) {
376
+ await organization.get();
377
+ }
378
+ return organization;
379
+ } catch (error) {
380
+ this.apiClient._logger.error(`[Api.${this.__entityTag}.organization] Erreur lors de la création d'une instance organization :`, error.message);
381
+ throw error;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Récupérer les projets d'un utilisateur : Récupère la liste des projets auxquels l'utilisateur contribue.
387
+ * Constant : GET_PROJECTS_ADMIN | GET_PROJECTS_NO_ADMIN
388
+ */
389
+ async getProjects(data = {}) {
390
+ delete data?.pathParams;
391
+
392
+ const fetchFn = this.isMe
393
+ ? () => this.callIsMe(() => this.endpointApi.getProjectsAdmin(data))
394
+ : () => this.endpointApi.getProjectsNoAdmin(data);
395
+
396
+ if (!this.isMe && !data.filters) {
397
+ data.filters = {
398
+ "$or": {
399
+ [`links.contributors.${this.id}`]: { "$exists": true },
400
+ [`parent.${this.id}`]: { "$exists": true }
401
+ },
402
+ [`links.contributors.${this.id}`]: { "$exists": true }
403
+ // TODO : revoir les filtres pour harmoniser avec orga (schema pris de cocolight)
404
+ };
405
+ }
406
+
407
+ const arrayObjetProjects = await fetchFn();
408
+
409
+ if (!Array.isArray(arrayObjetProjects.results)) {
410
+ throw new ApiResponseError("Erreur lors de la récupération des projets.", 500, arrayObjetProjects.results);
411
+ }
412
+
413
+ const rawProjectsList = arrayObjetProjects.results.map((projectData) =>
414
+ this.deps.Project.fromServerData(projectData, this, {
415
+ User,
416
+ News: this.deps.News,
417
+ EndpointApi: this.deps.EndpointApi
418
+ })
419
+ );
420
+
421
+ return {
422
+ count: arrayObjetProjects?.count?.projects ?? 0,
423
+ results: rawProjectsList
424
+ };
425
+ }
426
+
427
+ /**
428
+ * Crée une instance de projet et récupère son profil si nécessaire.
429
+ *
430
+ * @param {Object} projectData - Les données nécessaires pour initialiser le projet.
431
+ * @returns {Promise<Project>} Une promesse qui résout l'objet Projet créé.
432
+ * @throws {Error} Si une erreur se produit lors de la création du projet.
433
+ */
434
+ async project(projectData = {}, ) {
435
+ try {
436
+ const project = new this.deps.Project(this, projectData, { User, News: this.deps.News, EndpointApi : this.deps.EndpointApi });
437
+ if (projectData.id || projectData.slug) {
438
+ await project.get();
439
+ }
440
+ return project;
441
+ } catch (error) {
442
+ this.apiClient._logger.error(`[Api.${this.__entityTag}.project] Erreur lors de la création d'une instance project :`, error.message);
443
+ throw error;
444
+ }
445
+ }
190
446
 
191
447
  /**
192
- * Récupérer les actualités : Récupère la liste d’actualités selon plusieurs critères.
448
+ * Récupérer les actualités : Récupère la liste des actualités liées à l'utilisateur.
193
449
  * Constant : GET_NEWS
194
450
  */
195
451
  async getNews(data = {}) {
@@ -205,26 +461,76 @@ export class User {
205
461
  throw new ApiResponseError("Erreur lors de la récupération des actualités.", 500, arrayObjetNews);
206
462
  }
207
463
  const rawNewsList = arrayObjetNews.map((newsData) =>
208
- News.fromServerData(newsData, this, { User, EndpointApi: this.deps.EndpointApi })
464
+ this.deps.News.fromServerData(newsData, this, { User, EndpointApi: this.deps.EndpointApi })
209
465
  );
210
466
 
211
467
  return this._createFilteredProxy(rawNewsList);
212
468
  }
213
469
 
470
+ /**
471
+ * Crée une instance de news et la récupère si nécessaire.
472
+ *
473
+ * @param {Object} newsData - Les données nécessaires pour initialiser la news.
474
+ * @returns {Promise<News>} Une promesse qui résout l'objet News créé.
475
+ * @throws {Error} Si une erreur se produit lors de la création de la news.
476
+ */
214
477
  async news(newsData = {}, ) {
215
478
  try {
216
- const news = new News(this, newsData, { User, EndpointApi : this.deps.EndpointApi });
479
+ const news = new this.deps.News(this, newsData, { User, EndpointApi : this.deps.EndpointApi });
217
480
  if (newsData.id) {
218
481
  await news.get();
219
482
  }
220
483
  return news;
221
484
  } catch (error) {
222
- console.error("[Api.user.news] Erreur lors de la création d'une instance news :", error.message);
485
+ this.apiClient._logger.error(`[Api.${this.__entityTag}.news] Erreur lors de la création d'une instance news :`, error.message);
223
486
  throw error;
224
487
  }
225
488
  }
489
+
490
+ _updateInitialDraftSnapshot() {
491
+ this.#initialDraftData = JSON.parse(JSON.stringify(this.#draftData));
492
+ }
493
+
494
+ hasChanges() {
495
+ return JSON.stringify(this.#draftData) !== JSON.stringify(this.#initialDraftData);
496
+ }
497
+
498
+ /**
499
+ * Champs par défaut pour les schemas json ou l'on extrait les fields. (pour les if/else/then)
500
+ *
501
+ * @property {Object} defaultFields - Un objet contenant les propriétés par défaut pour l'entité User.
502
+ */
503
+ defaultFields = {
504
+ typeElement: this.getEntityType(),
505
+ };
506
+
507
+ /**
508
+ * Champs à supprimer de draft lors de la construction du proxy.
509
+ *
510
+ * @property {Array<string>} removeFields - Un tableau de chaînes représentant les champs à supprimer.
511
+ */
512
+ removeFields = [
513
+ "typeElement",
514
+ ];
515
+
516
+ /**
517
+ * Transformateurs appliqués lors de la lecture du draft.
518
+ *
519
+ * @type {Object<string, function>}
520
+ */
521
+ transforms = {
522
+ github: (val, full) => full?.socialNetwork?.github,
523
+ gitlab: (val, full) => full?.socialNetwork?.gitlab,
524
+ facebook: (val, full) => full?.socialNetwork?.facebook,
525
+ twitter: (val, full) => full?.socialNetwork?.twitter,
526
+ instagram: (val, full) => full?.socialNetwork?.instagram,
527
+ diaspora: (val, full) => full?.socialNetwork?.diaspora,
528
+ mastodon: (val, full) => full?.socialNetwork?.mastodon,
529
+ telegram: (val, full) => full?.socialNetwork?.telegram,
530
+ signal: (val, full) => full?.socialNetwork?.signal
531
+ };
226
532
  }
227
533
 
228
534
  // Incorporation des mixins dans User
229
- Object.assign(User.prototype, EntityMixin, UtilMixin, UserMixin, NewsMixin);
535
+ Object.assign(User.prototype, MutualEntityMixin, UtilMixin, UserMixin, DraftStateMixin);
230
536
 
@@ -1,26 +1,48 @@
1
1
  // UserApi.js
2
2
  import ApiClient from "../ApiClient.js";
3
- import { ApiResponseError } from "../error.js";
3
+ import { ApiError, ApiResponseError } from "../error.js";
4
4
  import EndpointApi from "./EndpointApi.js";
5
+ import { News } from "./News.js";
6
+ import { Organization } from "./Organization.js";
7
+ import { Project } from "./Project.js";
5
8
  import { User } from "./User.js";
6
9
 
7
10
  export class UserApi {
8
11
  constructor(options) {
9
12
  // Injection de dépendance : ApiClient est créé à partir des options
10
13
  this.client = new ApiClient(options);
14
+ // si l'option "tokenStorageStrategy" est définie, on l'utilise pour créer une instance de ApiClient
11
15
  this.loggedUser = null;
12
16
  }
17
+
18
+ get isConnected() {
19
+ return this.client.isConnected;
20
+ }
21
+ get userId() {
22
+ return this.client.userId;
23
+ }
24
+
13
25
  // Méthode d'authentification : récupère les données utilisateur depuis un endpoint
14
26
  async login(email, password) {
15
27
  return this.client.safeCall(async () => {
16
28
  // Appel à un endpoint d'authentification
17
29
  const response = await this.client.callEndpoint("AUTHENTICATE_URL", { email, password });
18
30
  // Création d'une instance de LoggedInUser à partir des données reçues
19
- this.loggedUser = new User(this.client, { id: response.data.user.id }, response.data.user, { EndpointApi });
31
+ this.loggedUser = new User(this.client, response.data.user, { EndpointApi, Organization, Project, News });
20
32
  return this.loggedUser;
21
33
  });
22
34
  }
23
35
 
36
+ async meIsconnected() {
37
+ if(!this.client.isConnected || !this.client.userId) {
38
+ throw new ApiError("User not connected", 401);
39
+ }
40
+ this.client._logger.info("UserApi", "meIsconnected", this.client.userId);
41
+ this.loggedUser = new User(this.client, { id: this.client.userId }, { EndpointApi, Organization, Project, News });
42
+ return this.loggedUser;
43
+ }
44
+
45
+
24
46
  async register({
25
47
  name,
26
48
  username,
@@ -46,4 +68,5 @@ export class UserApi {
46
68
  return response.data;
47
69
  });
48
70
  }
71
+
49
72
  }