@communecter/cocolight-api-client 1.0.22 → 1.0.24

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,3 +1,4 @@
1
+ import { ApiError, ApiResponseError } from "../error.js";
1
2
  import BaseEntity from "./BaseEntity.js";
2
3
 
3
4
  export class Project extends BaseEntity {
@@ -63,7 +64,7 @@ export class Project extends BaseEntity {
63
64
 
64
65
  async _add(payload) {
65
66
  if (!this._calledFromSave) {
66
- throw new Error("utilisation invalide de _add, utilisez save");
67
+ throw new ApiError("utilisation invalide de _add, utilisez save");
67
68
  }
68
69
 
69
70
  payload.id = this._newId?.();
@@ -87,8 +88,12 @@ export class Project extends BaseEntity {
87
88
  }
88
89
 
89
90
  async _update(payload) {
91
+ if(!this.isAdmin()){
92
+ throw new ApiError("Vous n'avez pas les droits pour modifier ce projet", 403);
93
+ }
94
+
90
95
  if (!this._calledFromSave) {
91
- throw new Error("utilisation invalide de _update, utilisez save");
96
+ throw new ApiError("utilisation invalide de _update, utilisez save");
92
97
  }
93
98
 
94
99
  if (payload.id) delete payload.id;
@@ -126,7 +131,7 @@ export class Project extends BaseEntity {
126
131
  }
127
132
 
128
133
  async getOrganizations() {
129
- throw new Error(`getOrganizations n'existe pas dans ${this.constructor.name}`);
134
+ throw new ApiError(`getOrganizations n'existe pas dans ${this.constructor.name}`);
130
135
  }
131
136
 
132
137
  async getProjects(data = {}) {
@@ -134,7 +139,7 @@ export class Project extends BaseEntity {
134
139
  }
135
140
 
136
141
  async getEvents() {
137
- throw new Error(`getEvents pas encore implémenté dans ${this.constructor.name}`);
142
+ throw new ApiError(`getEvents pas encore implémenté dans ${this.constructor.name}`);
138
143
  }
139
144
 
140
145
  async getPois(data = {}) {
@@ -153,6 +158,70 @@ export class Project extends BaseEntity {
153
158
  return super.getSubscribers(data);
154
159
  }
155
160
 
161
+ /**
162
+ * Récupérer les contributeurs d'un projet.
163
+ * Constant : GET_CONTRIBUTORS_ADMIN / GET_CONTRIBUTORS_NO_ADMIN
164
+ * @param {Object} data - Les données de requête.
165
+ * @param {Object} options - Options supplémentaires.
166
+ * @param {boolean} options.toBeValidated - Indique si les contributeurs doivent être validés.
167
+ * @param {boolean} options.isAdmin - Indique si l'utilisateur est admin.
168
+ * @param {boolean} options.isInviting - Indique si l'utilisateur est en attente d'invitation.
169
+ * @param {Array} options.roles - Liste des rôles à filtrer.
170
+ * @returns {Promise<Object>} - Un objet contenant le nombre de contributeurs et la liste des contributeurs.
171
+ * @throws {ApiResponseError} - Si une erreur se produit lors de la récupération des contributeurs.
172
+ *
173
+ * @example
174
+ * // Récupérer tous les contributeurs
175
+ * const contributors = await project.getContributors();
176
+ *
177
+ * // Récupérer les contributeurs avec validation en attente
178
+ * const contributorsToBeValidated = await project.getContributors({}, { toBeValidated: true });
179
+ *
180
+ * // Récupérer les contributeurs avec un rôle spécifique
181
+ * const contributorsWithRole = await project.getContributors({}, { roles: ["admin"] });
182
+ *
183
+ * // Récupérer les contributeurs administrateurs
184
+ * const adminContributors = await project.getContributors({}, { isAdmin: true });
185
+ *
186
+ * // Récupérer les contributeurs en attente d'invitation
187
+ * const invitingContributors = await project.getContributors({}, { isInviting: true });
188
+ *
189
+ */
190
+ async getContributors(data = {}, options = {}) {
191
+ const { toBeValidated, isAdmin, isInviting, isAdminPending, roles = [] } = options;
192
+
193
+ if(this.isMe){
194
+ data.pathParams = { type: this.getEntityType(), id: this.id };
195
+ // NOTE : dans le schema je crois que si pas de data.filters alors le default ce fait avec data.pathParams
196
+ // data.filters = {
197
+ // [`links.projects.${this.id}`]: { "$exists": true },
198
+ // [`links.projects.${this.id}.toBeValidated`]: { "$exists": false },
199
+ // [`links.projects.${this.id}.isInviting`]: { "$exists": false }
200
+ // };
201
+ data.filters = this._buildLinkFilters(this.id, { linkType: "projects", toBeValidated, isAdmin, isAdminPending, isInviting, roles });
202
+ } else {
203
+ delete data?.pathParams;
204
+ data.filters = this._buildLinkFilters(this.id, { linkType: "projects", toBeValidated: "false", isAdmin, isInviting, roles });
205
+ }
206
+
207
+ const fetchFn = this.isMe
208
+ ? () => this.callIsMe(() => this.endpointApi.getContributorsAdmin(data))
209
+ : () => this.endpointApi.getContributorsNoAdmin(data);
210
+
211
+ const arrayObjet = await fetchFn();
212
+ if (!Array.isArray(arrayObjet.results)) {
213
+ throw new ApiResponseError("Erreur lors de la récupération des contributeurs", 500, arrayObjet.results);
214
+ }
215
+
216
+ // lier les entités au objets
217
+ const rawList = this._linkEntities(arrayObjet.results);
218
+
219
+ return {
220
+ count: arrayObjet.count,
221
+ results: rawList
222
+ };
223
+ }
224
+
156
225
  /**
157
226
  * Crée une instance de projet et récupère son profil si nécessaire.
158
227
  *
@@ -161,7 +230,9 @@ export class Project extends BaseEntity {
161
230
  * @throws {Error} Si une erreur se produit lors de la création du projet.
162
231
  */
163
232
  async project(projectData = {}) {
164
- // TODO: Vérifier si l'utilisateur est admin du projet
233
+ if(!this.isAdmin()){
234
+ throw new ApiError("Vous n'avez pas les droits pour créer un projet dans ce projet", 403);
235
+ }
165
236
  return super.project(projectData);
166
237
  }
167
238
 
@@ -173,7 +244,9 @@ export class Project extends BaseEntity {
173
244
  * @throws {Error} Si une erreur se produit lors de la création du POI.
174
245
  */
175
246
  async poi(poiData = {}) {
176
- // TODO: Vérifier si l'utilisateur est admin de l'organisation
247
+ if(!this.isAdmin()){
248
+ throw new ApiError("Vous n'avez pas les droits pour créer un POI dans ce projet", 403);
249
+ }
177
250
  return super.poi(poiData);
178
251
  }
179
252
 
@@ -185,7 +258,9 @@ export class Project extends BaseEntity {
185
258
  * @throws {Error} Si une erreur se produit lors de la création de l'événement.
186
259
  */
187
260
  async event(eventData = {}) {
188
- // TODO: Vérifier si l'utilisateur est admin de l'organisation
261
+ if(!this.isAdmin()){
262
+ throw new ApiError("Vous n'avez pas les droits pour créer un événement dans ce projet", 403);
263
+ }
189
264
  return super.event(eventData);
190
265
  }
191
266
 
@@ -197,7 +272,9 @@ export class Project extends BaseEntity {
197
272
  * @throws {Error} Si une erreur se produit lors de la création du badge.
198
273
  */
199
274
  async badge(badgeData = {}) {
200
- // TODO: Vérifier si l'utilisateur est admin de l'organisation
275
+ if(!this.isAdmin()){
276
+ throw new ApiError("Vous n'avez pas les droits pour créer un badge dans ce projet", 403);
277
+ }
201
278
  return super.badge(badgeData);
202
279
  }
203
280
 
@@ -209,7 +286,80 @@ export class Project extends BaseEntity {
209
286
  * @throws {Error} Si une erreur se produit lors de la création de la news.
210
287
  */
211
288
  async news(newsData = {}) {
289
+ // TODO: qui peut créer une news sur le projet ?
212
290
  return super.news(newsData);
213
291
  }
214
292
 
293
+ /**
294
+ * ───────────────────────────────
295
+ * Lien utilisateur ↔ projet
296
+ * (rejoindre, valider, quitter, devenir admin)
297
+ * ───────────────────────────────
298
+ */
299
+
300
+ /**
301
+ * Envoie une demande pour rejoindre le projet en tant que contributeur.
302
+ * L'action est soumise à validation par un administrateur du projet.
303
+ *
304
+ * @returns {Promise<Object>} - Résultat de la requête d'adhésion.
305
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si le projet ne supporte pas l'action.
306
+ */
307
+ async requestToJoin(){
308
+ return super.requestToJoin();
309
+ }
310
+
311
+ /**
312
+ * Envoie une demande pour devenir administrateur du projet.
313
+ * L'action est soumise à validation par un administrateur existant.
314
+ *
315
+ * @returns {Promise<Object>} - Résultat de la requête d'administration.
316
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si le projet ne supporte pas l'action.
317
+ */
318
+ async requestToJoinAdmin(){
319
+ return super.requestToJoinAdmin();
320
+ }
321
+
322
+ /**
323
+ * Accepte une invitation à rejoindre le projet.
324
+ * Cette action valide un lien en attente avec l'option `isInviting`.
325
+ *
326
+ * @returns {Promise<Object>} - Résultat de la validation du lien.
327
+ * @throws {ApiError} - Si aucune invitation n'est en attente ou si le projet ne supporte pas l'action.
328
+ */
329
+ async acceptInvitation(){
330
+ return super.acceptInvitation();
331
+ }
332
+
333
+ /**
334
+ * Quitte le projet, que ce soit en tant que contributeur ou administrateur.
335
+ * Cette action supprime le lien entre l'utilisateur et le projet.
336
+ *
337
+ * @returns {Promise<Object>} - Résultat de la déconnexion.
338
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou non contributeur.
339
+ */
340
+ async leave() {
341
+ return super.leave();
342
+ }
343
+
344
+ /**
345
+ * Suivre un projet.
346
+ * Cette action permet à l'utilisateur de suivre le projet.
347
+ *
348
+ * @returns {Promise<Object>} - Résultat de l'action de suivi.
349
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité ne supporte pas l'action.
350
+ */
351
+ async follow() {
352
+ return super.follow();
353
+ }
354
+
355
+ /**
356
+ * Se désabonne d'un projet.
357
+ *
358
+ * @returns {Promise<Object>} - Résultat de la désinscription.
359
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
360
+ */
361
+ async unfollow() {
362
+ return super.unfollow();
363
+ }
364
+
215
365
  }
package/src/api/User.js CHANGED
@@ -81,7 +81,7 @@ export class User extends BaseEntity {
81
81
 
82
82
  super(parent, data, deps);
83
83
  }
84
-
84
+
85
85
  get slug() {
86
86
  return this._draftData.slug || null;
87
87
  }
@@ -90,6 +90,14 @@ export class User extends BaseEntity {
90
90
  return this.isConnected && this.userId === this.id;
91
91
  }
92
92
 
93
+ get parentIsMe() {
94
+ return super.isMe;
95
+ }
96
+
97
+ get isActingUser() {
98
+ return this.parentIsMe && !this.isMe;
99
+ }
100
+
93
101
  /**
94
102
  * Récupère le profil complet de l'utilisateur.
95
103
  * Si l'utilisateur est connecté, on appelle le endpoint ME_INFO_URL,
@@ -181,23 +189,7 @@ export class User extends BaseEntity {
181
189
  }
182
190
 
183
191
  static fromServerData(data, parent, deps) {
184
- const instance = new User(parent.apiClient, data, deps);
185
- instance._serverData = { ...data };
186
- // est ce que je besoin de ça si il est contruit dans le constructeur ?
187
- // il doit y avaoir une raison pour les autres objets
188
- const { draft, proxy } = instance._buildDraftAndProxy({
189
- data: { ...data, ...instance.defaultFields },
190
- serverData: instance._serverData,
191
- constant: User.SCHEMA_CONSTANTS,
192
- apiClient: instance.apiClient,
193
- transforms: instance.transforms,
194
- removeFields: instance.removeFields
195
- });
196
-
197
- instance._draftData = draft;
198
- instance.data = proxy;
199
-
200
- return instance;
192
+ return new User(parent.apiClient, data, deps);
201
193
  }
202
194
 
203
195
  /**
@@ -451,6 +443,18 @@ export class User extends BaseEntity {
451
443
  return filteredBadges;
452
444
  }
453
445
 
446
+ async user(userData) {
447
+ if(!this.isMe){
448
+ throw new ApiError("Vous devez être connecté et être l'utilisateur");
449
+ }
450
+ if (!userData.id && !userData.slug) {
451
+ throw new ApiError("Vous devez fournir un id ou un slug pour créer un User.");
452
+ }
453
+ const user = new User(this, userData, this.deps);
454
+ await user.get();
455
+ return user;
456
+ }
457
+
454
458
  /**
455
459
  * Crée une instance d'organisation et récupère son profil si nécessaire.
456
460
  *
@@ -535,6 +539,257 @@ export class User extends BaseEntity {
535
539
  return super.badge(badgeData);
536
540
  }
537
541
 
542
+ /**
543
+ * ───────────────────────────────
544
+ * Lien utilisateur ↔ utilisateur (ami)
545
+ * (demander, valider, se retirer)
546
+ * ───────────────────────────────
547
+ */
548
+
549
+ /**
550
+ * Envoie une demande d'amitié à cet utilisateur.
551
+ * L'utilisateur ciblé devra valider la demande pour établir la relation.
552
+ *
553
+ * @returns {Promise<Object>} - Résultat de la requête.
554
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'action est interdite.
555
+ */
556
+ async sendFriendRequest() {
557
+ if (!this.isActingUser) {
558
+ throw new ApiError("Vous devez être connecté pour envoyer une demande d'amis.");
559
+ }
560
+
561
+ this._checkLinkableEntity();
562
+ if (!this.id) {
563
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
564
+ }
565
+
566
+ const { connectTypeConnect } = this._getLinkMeta();
567
+ const userLink = this._getLinkFromConnectedUser();
568
+
569
+ if (!userLink) {
570
+ const data = {
571
+ parentType: this.getEntityType(),
572
+ parentId: this.id,
573
+ connectType: connectTypeConnect
574
+ };
575
+ const retour = await this.endpointApi.connect(data);
576
+ // TODO : reflechier au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
577
+ await this.userContext.refresh();
578
+ return retour;
579
+ }
580
+
581
+ if (userLink.isInviting && userLink.invitorId === this.id) {
582
+ return this.acceptFriendRequest();
583
+ }
584
+
585
+ if (userLink.isInviting && userLink.invitorId === this.userId) {
586
+ throw new ApiError("Vous avez déjà envoyé une demande d'amis à cet utilisateur.");
587
+ }
588
+
589
+ throw new ApiError("Vous êtes déjà connecté à cette entité.");
590
+ }
591
+
592
+ /**
593
+ * Accepte une demande d'amitié envoyée par cet utilisateur.
594
+ * Cette action établit un lien entre les deux utilisateurs.
595
+ *
596
+ * @returns {Promise<Object>} - Résultat de la validation du lien.
597
+ * @throws {ApiError} - Si aucune invitation n'est en attente ou si l'action est interdite.
598
+ */
599
+ async acceptFriendRequest() {
600
+ if (!this.isActingUser) {
601
+ throw new ApiError("Vous devez être connecté pour accepter une demande d'amitié.");
602
+ }
603
+
604
+ this._checkLinkableEntity();
605
+ if (!this.id) {
606
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
607
+ }
608
+
609
+ const userLink = this._getLinkFromConnectedUser();
610
+
611
+ if (userLink?.isInviting && userLink.invitorId === this.id) {
612
+ const data = {
613
+ parentType: this.getEntityType(),
614
+ parentId: this.id,
615
+ linkOption: "isInviting"
616
+ };
617
+ const retour = await this.endpointApi.linkValidate(data);
618
+ // TODO : reflechier au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
619
+ await this.userContext.refresh();
620
+ return retour;
621
+ }
622
+
623
+ throw new ApiError("Vous n'avez pas d'invitation à valider.");
624
+ }
625
+
626
+ /**
627
+ * Supprime la relation d'amitié avec cet utilisateur.
628
+ * Cette action annule tout lien existant entre les deux profils.
629
+ *
630
+ * @returns {Promise<Object>} - Résultat de la suppression.
631
+ * @throws {ApiError} - Si aucune relation n'existe.
632
+ */
633
+ async removeFriend() {
634
+ if (!this.isActingUser) {
635
+ throw new ApiError("Vous devez être connecté pour supprimer un ami.");
636
+ }
637
+
638
+ this._checkLinkableEntity();
639
+ if (!this.id) {
640
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
641
+ }
642
+
643
+ const { connectTypeDisconnect } = this._getLinkMeta();
644
+ const userLink = this._getLinkFromConnectedUser();
645
+
646
+ if (!userLink) {
647
+ throw new ApiError("Vous n'êtes pas connecté à cette entité.");
648
+ }
649
+
650
+ const data = {
651
+ parentType: this.getEntityType(),
652
+ parentId: this.id,
653
+ connectType: connectTypeDisconnect
654
+ };
655
+ const retour = await this.endpointApi.disconnect(data);
656
+ // TODO : reflechier au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
657
+ await this.userContext.refresh();
658
+ return retour;
659
+ }
660
+
661
+
662
+ requestToJoin() {
663
+ throw new ApiError("l'utilisation de requestToJoin n'est pas autorisée sur un utilisateur.");
664
+ }
665
+
666
+ requestToJoinAdmin() {
667
+ throw new ApiError("l'utilisation de requestToJoinAdmin n'est pas autorisée sur un utilisateur.");
668
+ }
669
+
670
+ acceptInvitation() {
671
+ throw new ApiError("l'utilisation de acceptInvitation n'est pas autorisée sur un utilisateur.");
672
+ }
673
+
674
+ leave() {
675
+ throw new ApiError("l'utilisation de leave n'est pas autorisée sur un utilisateur.");
676
+ }
677
+
678
+ /**
679
+ * Suivre un utilisateur
680
+ * Cette action permet à l'utilisateur connecté de suivre un autre utilisateur.
681
+ * Elle nécessite que l'utilisateur soit connecté.
682
+ *
683
+ * @returns {Promise<Object>} - Résultat de la requête.
684
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'action est interdite.
685
+ */
686
+ async follow() {
687
+ if (!this.isActingUser) {
688
+ throw new ApiError("Vous devez être connecté pour suivre un utilisateur.");
689
+ }
690
+ if (!this.id) {
691
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
692
+ }
693
+
694
+ const userLink = this.userContext?.serverData?.links?.["follows"]?.[this.id] || null;
695
+
696
+ if (!userLink) {
697
+ const data = {
698
+ parentType: this.getEntityType(),
699
+ parentId: this.id
700
+ };
701
+ const retour = await this.endpointApi.follow(data);
702
+ // TODO : reflechier au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
703
+ await this.userContext.refresh();
704
+ return retour;
705
+ }
706
+
707
+ throw new ApiError("Vous êtes déjà abonné à cet utilisateur.");
708
+ }
709
+
710
+ /**
711
+ * Se désabonner d'un utilisateur
712
+ * Cette action permet à l'utilisateur connecté de se désabonner d'un autre utilisateur.
713
+ * Elle nécessite que l'utilisateur soit connecté.
714
+ *
715
+ * @returns {Promise<Object>} - Résultat de la requête.
716
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'action est interdite.
717
+ */
718
+ async unfollow() {
719
+ if (!this.isActingUser) {
720
+ throw new ApiError("Vous devez être connecté pour vous désabonner d'un utilisateur.");
721
+ }
722
+ if (!this.id) {
723
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
724
+ }
725
+
726
+ const userLink = this.userContext?.serverData?.links?.["follows"]?.[this.id] || null;
727
+
728
+ if (userLink) {
729
+ const data = {
730
+ parentType: this.getEntityType(),
731
+ parentId: this.id,
732
+ connectType: "followers"
733
+ };
734
+ const retour = await this.endpointApi.disconnect(data);
735
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
736
+ await this.userContext.refresh();
737
+ return retour;
738
+ }
739
+
740
+ throw new ApiError("Vous n'êtes pas abonné à cet utilisateur.");
741
+ }
742
+
743
+ /**
744
+ * Vérifie si l'utilisateur connecté est ami avec cet utilisateur.
745
+ *
746
+ * @returns {boolean} - True si l'utilisateur connecté est ami, sinon false.
747
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté.
748
+ */
749
+ isFriend() {
750
+ if (!this.isActingUser) {
751
+ throw new ApiError("Vous devez être connecté pour vérifier si vous êtes ami.");
752
+ }
753
+ this._assertEntityType("citoyens");
754
+ const userLink = this._getLinkFromConnectedUser();
755
+ return this._validateUserLink(userLink);
756
+ }
757
+
758
+ /**
759
+ * Vérifie si l'utilisateur suit l'entité.
760
+ *
761
+ * @returns {boolean} - `true` si l'utilisateur suit l'entité, `false` sinon.
762
+ * @throws {ApiError}
763
+ */
764
+ isFollower() {
765
+ if (!this.isActingUser) {
766
+ throw new ApiError("Vous devez être connecté pour vérifier si il vous suit.");
767
+ }
768
+ if (!this.id) {
769
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
770
+ }
771
+ this._assertEntityType("citoyens");
772
+ return this._isLinked("followers");
773
+ }
774
+
775
+ /**
776
+ * Vérifie si l'utilisateur est abonné à l'entité.
777
+ *
778
+ * @returns {boolean} - `true` si l'utilisateur est abonné, `false` sinon.
779
+ * @throws {ApiError}
780
+ */
781
+ isFollowing() {
782
+ if (!this.isActingUser) {
783
+ throw new ApiError("Vous devez être connecté pour vérifier si vous le suivez.");
784
+ }
785
+ if (!this.id) {
786
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
787
+ }
788
+ this._assertEntityType("citoyens");
789
+ return this._isLinked("follows");
790
+ }
791
+
792
+
538
793
  }
539
794
 
540
795
  // Incorporation des mixins dans User