@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.
@@ -4,8 +4,13 @@ import ObjectID from "bson-objectid";
4
4
  import pkg from "file-type";
5
5
 
6
6
  import { ApiAuthenticationError, ApiError, ApiResponseError, ApiValidationError } from "../error.js";
7
+ import { autoSyncDraftFromSchema, reactive } from "../utils/reactive.js";
7
8
  const { fromBuffer } = pkg;
8
9
 
10
+ /**
11
+ * @typedef {import("../ApiClient.js").default} ApiClient
12
+ * @typedef {import("./EndpointApi.js").default} EndpointApi
13
+ */
9
14
 
10
15
  /**
11
16
  * Classe de base pour toutes les entités métiers : utilisateurs, projets, organisations, etc.
@@ -26,6 +31,9 @@ export class BaseEntity {
26
31
  /** @type {boolean} Indique si `save()` est en cours */
27
32
  _calledFromSave = false;
28
33
 
34
+ /** @type {boolean} Indique si le draft est synchronisé avec le serveur */
35
+ _syncReactiveDraft = false;
36
+
29
37
  /**
30
38
  * Constructeur de l'entité.
31
39
  * @param {Object} parent - L'ApiClient ou une entité parente.
@@ -50,23 +58,29 @@ export class BaseEntity {
50
58
  this.deps = deps;
51
59
 
52
60
  if (parent?.__entityTag === "ApiClient") {
53
- /** @type {import("../ApiClient.js").default} */
61
+ /** @type {ApiClient} */
54
62
  this.apiClient = parent;
55
63
  this.parent = null;
64
+ this.userContext = null;
56
65
  } else if (parent?.apiClient) {
57
- /** @type {import("../ApiClient.js").default} */
66
+ /** @type {ApiClient} */
58
67
  this.apiClient = parent.apiClient;
59
68
  this.parent = parent;
69
+ if (parent?.__entityTag === "User") {
70
+ this.userContext = parent;
71
+ } else if (parent?.userContext) {
72
+ this.userContext = parent.userContext;
73
+ }
60
74
  } else {
61
75
  throw new ApiError("Parent invalide ou ApiClient manquant.");
62
76
  }
63
77
 
64
78
  // Gérer les deux cas : fonction constructeur ou instance
65
79
  if (typeof deps.EndpointApi === "function") {
66
- /** @type {import("./EndpointApi.js").default} */
80
+ /** @type {EndpointApi} */
67
81
  this.endpointApi = new deps.EndpointApi(this.apiClient);
68
82
  } else if (typeof deps.EndpointApi === "object") {
69
- /** @type {import("./EndpointApi.js").default} */
83
+ /** @type {EndpointApi} */
70
84
  this.endpointApi = deps.EndpointApi;
71
85
  } else {
72
86
  throw new ApiError("deps.EndpointApi doit être une classe ou une instance valide.");
@@ -93,6 +107,11 @@ export class BaseEntity {
93
107
  return this._draftData.id || null;
94
108
  }
95
109
 
110
+ /** @returns {string|null} Slug de l'entité */
111
+ get slug() {
112
+ return this._draftData.slug || null;
113
+ }
114
+
96
115
  /** Définit un ID (utilisé en interne) */
97
116
  _id(newId) {
98
117
  this._draftData.id = newId;
@@ -125,7 +144,7 @@ export class BaseEntity {
125
144
 
126
145
  /** @returns {boolean} Indique si cette entité représente l'utilisateur connecté */
127
146
  get isMe() {
128
- return this.isConnected && this.userId === this.parent?.id;
147
+ return this.isConnected && this.userId === this.userContext?.id;
129
148
  }
130
149
 
131
150
  /** @returns {string} Type de l'entité (ex: 'citoyens') */
@@ -162,6 +181,10 @@ export class BaseEntity {
162
181
 
163
182
  if (!this.id && typeof this._add === "function") {
164
183
  await this._add(payload);
184
+ // on refresh le contexte utilisateur si besoin
185
+ if(this.userContext) {
186
+ await this.userContext.refresh();
187
+ }
165
188
  return await this.refresh();
166
189
  } else if (typeof this._update === "function") {
167
190
  const hasChanged = await this._update(payload);
@@ -184,19 +207,7 @@ export class BaseEntity {
184
207
  */
185
208
  static fromServerData(data, parent, deps) {
186
209
  const instance = new this(parent, {}, deps);
187
- instance._serverData = { ...data };
188
-
189
- const { draft, proxy } = instance._buildDraftAndProxy({
190
- data: { ...data, ...instance.defaultFields },
191
- serverData: instance._serverData,
192
- constant: this.SCHEMA_CONSTANTS,
193
- apiClient: instance.apiClient,
194
- transforms: instance.transforms,
195
- removeFields: instance.removeFields
196
- });
197
-
198
- instance._draftData = draft;
199
- instance.data = proxy;
210
+ instance._setData(data);
200
211
  return instance;
201
212
  }
202
213
 
@@ -208,7 +219,10 @@ export class BaseEntity {
208
219
  * @private
209
220
  */
210
221
  _setData(newData) {
211
- this._serverData = { ...newData };
222
+ if (this.userContext && this.userContext !== this) {
223
+ this.apiClient._logger?.info?.(`[${this.__entityTag}] Mise à jour liée à userContext : ${this.userContext.id}`);
224
+ }
225
+ this._serverData = reactive({ ...newData });
212
226
 
213
227
  const { draft, proxy } = this._buildDraftAndProxy({
214
228
  data: { ...newData, ...this.defaultFields },
@@ -221,6 +235,10 @@ export class BaseEntity {
221
235
  this._initialDraftData = JSON.parse(JSON.stringify(draft));
222
236
  this._draftData = draft;
223
237
  this.data = proxy;
238
+
239
+ if (this._syncReactiveDraft) {
240
+ this.setupReactiveSync();
241
+ }
224
242
  }
225
243
 
226
244
 
@@ -831,6 +849,25 @@ export class BaseEntity {
831
849
  return Object.keys(changed).length > 0 ? changed : null;
832
850
  }
833
851
 
852
+ /**
853
+ * ───────────────────────────────
854
+ * ReactiveMixin
855
+ * ───────────────────────────────
856
+ */
857
+
858
+ /**
859
+ * Active la synchronisation réactive entre le brouillon et le schéma.
860
+ *
861
+ * @returns {void}
862
+ * @public
863
+ */
864
+ setupReactiveSync() {
865
+ if (!this._syncReactiveDraft) {
866
+ this._syncReactiveDraft = true;
867
+ }
868
+ autoSyncDraftFromSchema(this);
869
+ }
870
+
834
871
  /**
835
872
  * ───────────────────────────────
836
873
  * MutualEntityMixin
@@ -1101,8 +1138,8 @@ export class BaseEntity {
1101
1138
 
1102
1139
  const fetchKeysByEntity = {
1103
1140
  citoyens: ["id", "slug"],
1104
- organisations: ["id", "slug"],
1105
- projets: ["id", "slug"],
1141
+ organizations: ["id", "slug"],
1142
+ projects: ["id", "slug"],
1106
1143
  events: ["id", "slug"],
1107
1144
  poi: ["id", "slug"],
1108
1145
  news: ["id"],
@@ -1122,6 +1159,307 @@ export class BaseEntity {
1122
1159
  }
1123
1160
  }
1124
1161
 
1162
+ /**
1163
+ * Construit dynamiquement des filtres sur un lien entre entités
1164
+ *
1165
+ * @param {string} id - L'ID de l'entité cible.
1166
+ * @param {Object} options - Options de filtrage.
1167
+ * @param {"memberOf" | "projects"} [options.linkType] - Le type de lien (ex: "memberOf", "projects", etc.).
1168
+ * @param {boolean} [options.isAdmin] - Si défini, filtre selon l'existence de isAdmin.
1169
+ * @param {boolean} [options.isAdminPending] - Si défini, filtre selon l'existence de isAdminPending.
1170
+ * @param {boolean} [options.isInviting] - Si défini, filtre selon l'existence de isInviting.
1171
+ * @param {boolean} [options.toBeValidated] - Si défini, filtre selon l'existence de toBeValidated.
1172
+ * @param {Array<string>} [options.roles] - Rôles à filtrer.
1173
+ * @returns {Object} - Un objet de filtres à passer à une requête.
1174
+ * @throws {TypeError} - Si les types des paramètres ne sont pas valides.
1175
+ * @private
1176
+ */
1177
+ _buildLinkFilters(id, {
1178
+ linkType,
1179
+ isAdmin,
1180
+ isAdminPending,
1181
+ isInviting,
1182
+ toBeValidated = false,
1183
+ roles = []
1184
+ } = {}) {
1185
+ if (typeof id !== "string") throw new TypeError("id doit être une chaîne.");
1186
+ if (typeof linkType !== "string") throw new TypeError("linkType doit être une chaîne.");
1187
+ if (typeof isAdmin !== "undefined" && typeof isAdmin !== "boolean") {
1188
+ throw new TypeError("isAdmin doit être un booléen.");
1189
+ }
1190
+ if (typeof isAdminPending !== "undefined" && typeof isAdminPending !== "boolean") {
1191
+ throw new TypeError("isAdminPending doit être un booléen.");
1192
+ }
1193
+ if (typeof isInviting !== "undefined" && typeof isInviting !== "boolean") {
1194
+ throw new TypeError("isInviting doit être un booléen.");
1195
+ }
1196
+ if (typeof toBeValidated !== "undefined" && typeof toBeValidated !== "boolean") {
1197
+ throw new TypeError("toBeValidated doit être un booléen.");
1198
+ }
1199
+ if (!Array.isArray(roles)) {
1200
+ throw new TypeError("roles doit être un tableau de chaînes.");
1201
+ }
1202
+
1203
+ const path = `links.${linkType}.${id}`;
1204
+ const filters = {
1205
+ [`${path}`]: { $exists: true }
1206
+ };
1207
+
1208
+ if (typeof toBeValidated === "boolean") {
1209
+ filters[`${path}.toBeValidated`] = { $exists: toBeValidated };
1210
+ }
1211
+
1212
+ if (typeof isInviting === "boolean") {
1213
+ filters[`${path}.isInviting`] = { $exists: isInviting };
1214
+ }
1215
+
1216
+ if (isAdmin === true) {
1217
+ filters[`${path}.isAdmin`] = { $exists: true };
1218
+ }
1219
+
1220
+ if(isAdminPending === true) {
1221
+ filters[`${path}.isAdminPending`] = { $exists: true };
1222
+ }
1223
+
1224
+ if (roles.length > 0) {
1225
+ filters[`${path}.roles`] = { $in: roles };
1226
+ }
1227
+
1228
+ return filters;
1229
+ }
1230
+
1231
+ /**
1232
+ * Retourne les métadonnées de lien pour une entité :
1233
+ * - `linkType` : clé dans `serverData.links`
1234
+ * - `connectTypeConnect` : valeur envoyée pour `connect()`
1235
+ * - `connectTypeDisconnect` : valeur envoyée pour `disconnect()`
1236
+ * @typedef {Object} LinkMeta
1237
+ * @property {string} linkType
1238
+ * @property {string} connectTypeConnect
1239
+ * @property {string} connectTypeDisconnect
1240
+ * @returns {LinkMeta}
1241
+ * @throws {ApiError} - Si le type d'entité est inconnu.
1242
+ * @private
1243
+ */
1244
+ _getLinkMeta() {
1245
+ const map = {
1246
+ organizations: {
1247
+ linkType: "memberOf",
1248
+ connectTypeConnect: "member",
1249
+ connectTypeDisconnect: "members"
1250
+ },
1251
+ projects: {
1252
+ linkType: "projects",
1253
+ connectTypeConnect: "contributor",
1254
+ connectTypeDisconnect: "contributors"
1255
+ },
1256
+ events: {
1257
+ linkType: "events",
1258
+ connectTypeConnect: "attendee",
1259
+ connectTypeDisconnect: "attendees"
1260
+ },
1261
+ citoyens: {
1262
+ linkType: "friends",
1263
+ connectTypeConnect: "friend",
1264
+ connectTypeDisconnect: "friends"
1265
+ }
1266
+ };
1267
+
1268
+ const entityType = this.getEntityType();
1269
+ const meta = map[entityType];
1270
+
1271
+ if (!meta) {
1272
+ throw new ApiError(`Aucune correspondance de lien définie pour le type d'entité "${entityType}"`);
1273
+ }
1274
+
1275
+ return meta;
1276
+ }
1277
+
1278
+ /**
1279
+ * Vérifie si l'entité prend en charge les opérations de lien (`connect`, `disconnect`, etc.).
1280
+ * Utilise `_getLinkMeta()` comme source unique de vérité.
1281
+ *
1282
+ * @throws {ApiError} - Si l'entité ne supporte pas les liens.
1283
+ * @private
1284
+ */
1285
+ _checkLinkableEntity() {
1286
+ try {
1287
+ this._getLinkMeta(); // si l'entité n'est pas supportée, ça throw déjà
1288
+ } catch (error) {
1289
+ if (error instanceof ApiError) {
1290
+ throw new ApiError(`L'entité "${this.getEntityType()}" ne prend pas en charge les actions de lien.`);
1291
+ }
1292
+ throw error;
1293
+ }
1294
+ }
1295
+
1296
+ /**
1297
+ * Retourne le lien de l'utilisateur connecté avec l'entité cible (dans `parent.serverData.links`)
1298
+ * @private
1299
+ */
1300
+ _getLinkFromConnectedUser() {
1301
+ const { linkType } = this._getLinkMeta();
1302
+ return this?.userContext?.serverData?.links?.[linkType]?.[this.id] || null;
1303
+ }
1304
+
1305
+ /**
1306
+ * Soumet une demande de connexion à une entité (ex : membre, contributeur),
1307
+ * ou valide l'invitation si elle existe déjà.
1308
+ *
1309
+ * @returns {Promise<Object>} - Résultat de l'API
1310
+ * @throws {ApiError} - En cas d'erreur de connexion ou d'invitation.
1311
+ * @private
1312
+ */
1313
+ async _submitLinkRequest() {
1314
+
1315
+ if (!this.id) {
1316
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
1317
+ }
1318
+
1319
+ const { connectTypeConnect } = this._getLinkMeta();
1320
+
1321
+ const userLink = this._getLinkFromConnectedUser();
1322
+
1323
+ // Cas : aucun lien → on demande à se connecter
1324
+ if (!userLink) {
1325
+ const data = {
1326
+ parentType: this.getEntityType(),
1327
+ parentId: this.id,
1328
+ connectType: connectTypeConnect
1329
+ };
1330
+
1331
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
1332
+ const retour = await this.callIsMe(() => this.endpointApi.connect(data));
1333
+ await this.userContext.refresh();
1334
+ return retour;
1335
+ }
1336
+
1337
+ // Cas : invitation reçue → on valide l'invitation
1338
+ if (userLink.isInviting) {
1339
+ return this._acceptLinkRequest();
1340
+ }
1341
+
1342
+ // Cas : déjà en attente
1343
+ if (userLink.toBeValidated) {
1344
+ throw new ApiError("Vous êtes déjà en attente de validation.");
1345
+ }
1346
+
1347
+ // Cas par défaut : rien à faire
1348
+ throw new ApiError("Vous êtes déjà connecté à cette entité.");
1349
+ }
1350
+
1351
+ /**
1352
+ * Soumet une demande pour devenir administrateur d'une entité.
1353
+ *
1354
+ * @returns {Promise<Object>}
1355
+ * @throws {ApiError}
1356
+ * @private
1357
+ */
1358
+ async _submitLinkRequestAdmin() {
1359
+
1360
+ if (!this.id) {
1361
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
1362
+ }
1363
+
1364
+ const userLink = this._getLinkFromConnectedUser();
1365
+
1366
+ // Aucun lien existant → envoie une demande avec rôle "admin"
1367
+ if (!userLink) {
1368
+ const data = {
1369
+ parentType: this.getEntityType(),
1370
+ parentId: this.id,
1371
+ connectType: "admin"
1372
+ };
1373
+
1374
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
1375
+ const retour = await this.callIsMe(() => this.endpointApi.connect(data));
1376
+ await this.userContext.refresh();
1377
+ return retour;
1378
+ }
1379
+
1380
+ // Invitation reçue → accepte automatiquement
1381
+ if (userLink.isInviting) {
1382
+ return this._acceptLinkRequest();
1383
+ }
1384
+
1385
+ // Déjà en attente pour admin
1386
+ if (userLink.toBeValidated && userLink.isAdminPending) {
1387
+ throw new ApiError("Vous êtes déjà en attente de validation pour devenir admin.");
1388
+ }
1389
+
1390
+ // Déjà en attente pour un autre rôle
1391
+ if (userLink.toBeValidated) {
1392
+ throw new ApiError("Vous êtes déjà en attente de validation pour rejoindre cette entité.");
1393
+ }
1394
+
1395
+ // Déjà connecté
1396
+ throw new ApiError("Vous êtes déjà connecté à cette entité.");
1397
+ }
1398
+
1399
+ /**
1400
+ * Accepte une demande de lien (ex : invitation à rejoindre un groupe).
1401
+ *
1402
+ * @returns {Promise<Object>} - Résultat de l'API
1403
+ * @throws {ApiError} - En cas d'erreur de connexion ou d'invitation.
1404
+ * @private
1405
+ */
1406
+ async _acceptLinkRequest() {
1407
+
1408
+ if (!this.id) {
1409
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
1410
+ }
1411
+
1412
+ const userLink = this._getLinkFromConnectedUser();
1413
+
1414
+ if (userLink.isInviting) {
1415
+ const data = {
1416
+ parentType: this.getEntityType(),
1417
+ parentId: this.id,
1418
+ linkOption: "isInviting"
1419
+ };
1420
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
1421
+ const retour = await this.callIsMe(() => this.endpointApi.linkValidate(data));
1422
+ await this.userContext.refresh();
1423
+ return retour;
1424
+ }
1425
+
1426
+ throw new ApiError("Vous n'avez pas d'invitation à valider.");
1427
+ }
1428
+
1429
+ /**
1430
+ * Annule une demande de lien ou se déconnecte d'une entité.
1431
+ *
1432
+ * @returns {Promise<Object>} - Résultat de l'API
1433
+ * @throws {ApiError} - En cas d'erreur de connexion ou d'invitation.
1434
+ * @private
1435
+ */
1436
+ async _leaveLinkRequest() {
1437
+
1438
+ if (!this.id) {
1439
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
1440
+ }
1441
+
1442
+ const { connectTypeDisconnect } = this._getLinkMeta();
1443
+
1444
+ const userLink = this._getLinkFromConnectedUser();
1445
+
1446
+ if (!userLink) {
1447
+ throw new ApiError("Vous n'êtes pas connecté à cette entité.");
1448
+ }
1449
+
1450
+ const data = {
1451
+ parentType: this.getEntityType(),
1452
+ parentId: this.id,
1453
+ // Normalement en auto dans le schema mais je le met quand même
1454
+ connectType: connectTypeDisconnect
1455
+ };
1456
+
1457
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
1458
+ const retour = await this.callIsMe(() => this.endpointApi.disconnect(data));
1459
+ await this.userContext.refresh();
1460
+ return retour;
1461
+ }
1462
+
1125
1463
  /**
1126
1464
  * ───────────────────────────────
1127
1465
  * EntityMixin
@@ -1543,6 +1881,289 @@ export class BaseEntity {
1543
1881
  return this._createFilteredProxy(rawList);
1544
1882
  }
1545
1883
 
1884
+ /**
1885
+ * Soumet une demande pour rejoindre l'entité courante (ex. organisation, projet, événement...).
1886
+ * Si une invitation est en attente, elle est automatiquement acceptée.
1887
+ *
1888
+ * @returns {Promise<Object>} - Résultat de la demande ou de la validation.
1889
+ * @throws {ApiError} - Si l'entité ne supporte pas l'action ou si une demande est déjà en cours.
1890
+ */
1891
+ async requestToJoin(){
1892
+ if (!this.isMe) {
1893
+ throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.");
1894
+ }
1895
+ this._checkLinkableEntity();
1896
+ return this._submitLinkRequest();
1897
+ }
1898
+
1899
+ /**
1900
+ * Soumet une demande pour devenir administrateur de l'entité courante.
1901
+ * Si une invitation est en attente, elle est automatiquement validée.
1902
+ *
1903
+ * @returns {Promise<Object>} - Résultat de la demande ou de la validation.
1904
+ * @throws {ApiError} - Si l'entité ne supporte pas l'action ou si une demande est déjà en cours.
1905
+ */
1906
+ async requestToJoinAdmin(){
1907
+ if (!this.isMe) {
1908
+ throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.");
1909
+ }
1910
+ this._checkLinkableEntity();
1911
+ return this._submitLinkRequestAdmin();
1912
+ }
1913
+
1914
+ /**
1915
+ * Accepte une invitation à rejoindre l'entité courante.
1916
+ * Ne fonctionne que si un lien avec `isInviting` est détecté.
1917
+ *
1918
+ * @returns {Promise<Object>} - Résultat de l'acceptation de l'invitation.
1919
+ * @throws {ApiError} - Si aucune invitation n'est en attente ou si l'entité ne supporte pas cette action.
1920
+ */
1921
+ async acceptInvitation(){
1922
+ if (!this.isMe) {
1923
+ throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.");
1924
+ }
1925
+ this._checkLinkableEntity();
1926
+ return this._acceptLinkRequest();
1927
+ }
1928
+
1929
+ /**
1930
+ * Se désengage de l'entité courante : quitte un rôle (membre, contributeur, etc.).
1931
+ *
1932
+ * @returns {Promise<Object>} - Résultat de la déconnexion.
1933
+ * @throws {ApiError} - Si l'entité ne supporte pas l'action ou si l'utilisateur n'est pas lié à cette entité.
1934
+ */
1935
+ async leave() {
1936
+ if (!this.isMe) {
1937
+ throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.");
1938
+ }
1939
+ this._checkLinkableEntity();
1940
+ return this._leaveLinkRequest();
1941
+ }
1942
+
1943
+ /**
1944
+ * S'abonne à l'entité courante.
1945
+ *
1946
+ * @returns {Promise<Object>} - Résultat de l'abonnement.
1947
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
1948
+ */
1949
+ async follow() {
1950
+ if (!this.isMe) {
1951
+ throw new ApiError("Vous devez être connecté pour suivre cette entité.");
1952
+ }
1953
+
1954
+ if (!this.id) {
1955
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
1956
+ }
1957
+
1958
+ const userLink = this.userContext?.serverData?.links?.["follows"]?.[this.id] || null;
1959
+
1960
+ if (!userLink) {
1961
+ const data = {
1962
+ parentType: this.getEntityType(),
1963
+ parentId: this.id
1964
+ };
1965
+ const retour = await this.callIsMe(() => this.endpointApi.follow(data));
1966
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
1967
+ await this.userContext.refresh();
1968
+ return retour;
1969
+ }
1970
+
1971
+ throw new ApiError("Vous êtes déjà abonné à cette entité.");
1972
+ }
1973
+
1974
+ /**
1975
+ * Se désabonne de l'entité courante.
1976
+ *
1977
+ * @returns {Promise<Object>} - Résultat de la désinscription.
1978
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
1979
+ */
1980
+ async unfollow() {
1981
+ if (!this.isMe) {
1982
+ throw new ApiError("Vous devez être connecté pour vous désabonner.");
1983
+ }
1984
+
1985
+ if (!this.id) {
1986
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
1987
+ }
1988
+
1989
+ const userLink = this.userContext?.serverData?.links?.["follows"]?.[this.id] || null;
1990
+
1991
+ if (userLink) {
1992
+ const data = {
1993
+ parentType: this.getEntityType(),
1994
+ parentId: this.id,
1995
+ connectType: "followers"
1996
+ };
1997
+ const retour = await this.callIsMe(() => this.endpointApi.disconnect(data));
1998
+ // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
1999
+ await this.userContext.refresh();
2000
+ return retour;
2001
+ }
2002
+
2003
+ throw new ApiError("Vous n'êtes pas abonné à cette entité.");
2004
+ }
2005
+
2006
+
2007
+ /**
2008
+ * Vérifie si l'utilisateur est connecté et a accès à l'entité.
2009
+ *
2010
+ * @param {string} action - Action à effectuer.
2011
+ * @returns {void}
2012
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
2013
+ * @private
2014
+ */
2015
+ _checkAccess(action = "effectuer cette action") {
2016
+ if (!this.isMe) {
2017
+ throw new ApiError(`Vous devez être connecté pour ${action}`);
2018
+ }
2019
+ if (!this.id) {
2020
+ throw new ApiError(`${this.constructor.name} non enregistrée.`);
2021
+ }
2022
+ }
2023
+
2024
+ /**
2025
+ * Vérifie si l'utilisateur a un lien valide avec l'entité.
2026
+ *
2027
+ * @param {Object} userLink - Lien de l'utilisateur avec l'entité.
2028
+ * @param {boolean} userLink.toBeValidated - Indique si le lien est en attente de validation.
2029
+ * @param {boolean} userLink.isInviting - Indique si l'utilisateur a été invité.
2030
+ * @returns {boolean} - `true` si le lien est valide, `false` sinon.
2031
+ * @private
2032
+ */
2033
+ _validateUserLink(userLink) {
2034
+ if (!userLink) return false;
2035
+ const { toBeValidated, isInviting } = userLink;
2036
+ return !toBeValidated && !isInviting;
2037
+ }
2038
+
2039
+ /**
2040
+ * Vérifie si l'entité est d'un type spécifique.
2041
+ *
2042
+ * @param {...string} types - Types d'entité attendus.
2043
+ * @throws {ApiError} - Si l'entité n'est pas du type attendu.
2044
+ * @private
2045
+ */
2046
+ _assertEntityType(...types) {
2047
+ const expectedTypes = Array.isArray(types[0]) ? types[0] : types;
2048
+ if (!expectedTypes.includes(this.getEntityType())) {
2049
+ throw new ApiError(`L'entité doit être de type : ${expectedTypes.join(", ")}, reçu : ${this.getEntityType()}`);
2050
+ }
2051
+ }
2052
+
2053
+ /**
2054
+ * Vérifie si l'entité est liée à un type de lien spécifique.
2055
+ *
2056
+ * @param {string} linkType - Type de lien à vérifier.
2057
+ * @returns {boolean} - `true` si l'entité est liée, `false` sinon.
2058
+ * @private
2059
+ */
2060
+ _isLinked(linkType) {
2061
+ return !!this.userContext?.serverData?.links?.[linkType]?.[this.id];
2062
+ }
2063
+
2064
+ /**
2065
+ * Vérifie si l'utilisateur est l'auteur de l'entité.
2066
+ *
2067
+ * @returns {boolean} - `true` si l'utilisateur est l'auteur, `false` sinon.
2068
+ * @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si les données du serveur ne sont pas disponibles.
2069
+ */
2070
+ isAuthor() {
2071
+ this._checkAccess("vérifier l'auteur");
2072
+ if (!this.serverData) {
2073
+ throw new ApiError("Aucune donnée serveur disponible.");
2074
+ }
2075
+ return this.userId && this.serverData?.creator === this.userId;
2076
+ }
2077
+
2078
+ /**
2079
+ * Vérifie si l'utilisateur est administrateur de l'entité.
2080
+ *
2081
+ * @returns {boolean} - `true` si l'utilisateur est administrateur, `false` sinon.
2082
+ * @throws {ApiError}
2083
+ */
2084
+ isAdmin() {
2085
+ this._checkAccess("vérifier l'administrateur.");
2086
+ this._assertEntityType("organizations", "projects", "events");
2087
+ const userLink = this._getLinkFromConnectedUser();
2088
+ return this._validateUserLink(userLink) && userLink?.isAdmin === true && !userLink?.isAdminPending;
2089
+ }
2090
+
2091
+ /**
2092
+ * Vérifie si l'utilisateur est soit l'auteur, soit administrateur de l'entité.
2093
+ *
2094
+ * @returns {boolean} - `true` si l'utilisateur est l'auteur ou administrateur, `false` sinon.
2095
+ * @throws {ApiError}
2096
+ */
2097
+ isAuthorOrAdmin() {
2098
+ this._checkAccess("vérifier l'auteur ou l'administrateur.");
2099
+ return this.isAuthor() || this.isAdmin();
2100
+ }
2101
+
2102
+ /**
2103
+ * Vérifie si l'utilisateur est membre de l'entité.
2104
+ *
2105
+ * @returns {boolean} - `true` si l'utilisateur est membre, `false` sinon.
2106
+ * @throws {ApiError}
2107
+ */
2108
+ isMember() {
2109
+ this._checkAccess("vérifier le membre.");
2110
+ this._assertEntityType("organizations");
2111
+ const userLink = this._getLinkFromConnectedUser();
2112
+ return this._validateUserLink(userLink);
2113
+ }
2114
+
2115
+ /**
2116
+ * Vérifie si l'utilisateur est contributeur de l'entité.
2117
+ *
2118
+ * @returns {boolean} - `true` si l'utilisateur est contributeur, `false` sinon.
2119
+ * @throws {ApiError}
2120
+ */
2121
+ isContributor() {
2122
+ this._checkAccess("vérifier le contributeur.");
2123
+ this._assertEntityType("projects");
2124
+ const userLink = this._getLinkFromConnectedUser();
2125
+ return this._validateUserLink(userLink);
2126
+ }
2127
+
2128
+ /**
2129
+ * Vérifie si l'utilisateur est participant de l'entité.
2130
+ *
2131
+ * @returns {boolean} - `true` si l'utilisateur est participant, `false` sinon.
2132
+ * @throws {ApiError}
2133
+ */
2134
+ isAttendee() {
2135
+ this._checkAccess("vérifier si vous êtes un participant.");
2136
+ this._assertEntityType("events");
2137
+ const userLink = this._getLinkFromConnectedUser();
2138
+ return this._validateUserLink(userLink);
2139
+ }
2140
+
2141
+ /**
2142
+ * Vérifie si l'utilisateur suit l'entité.
2143
+ *
2144
+ * @returns {boolean} - `true` si l'utilisateur suit l'entité, `false` sinon.
2145
+ * @throws {ApiError}
2146
+ */
2147
+ isFollower() {
2148
+ this._checkAccess("vérifier si il vous suit.");
2149
+ this._assertEntityType("citoyens","organizations", "projects", "events", "poi");
2150
+ return this._isLinked("followers");
2151
+ }
2152
+
2153
+ /**
2154
+ * Vérifie si l'utilisateur est abonné à l'entité.
2155
+ *
2156
+ * @returns {boolean} - `true` si l'utilisateur est abonné, `false` sinon.
2157
+ * @throws {ApiError}
2158
+ */
2159
+ isFollowing() {
2160
+ this._checkAccess("vérifier si vous le suivez.");
2161
+ this._assertEntityType("citoyens","organizations", "projects", "events", "poi");
2162
+ return this._isLinked("follows");
2163
+ }
2164
+
2165
+
2166
+
1546
2167
  }
1547
2168
 
1548
2169
  export default BaseEntity;