@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.
- package/dist/cocolight-api-client.browser.js +2 -2
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +1 -1
- package/package.json +1 -1
- package/src/api/Badge.js +20 -12
- package/src/api/BaseEntity.js +642 -21
- package/src/api/EndpointApi.js +20 -4
- package/src/api/EndpointApi.types.d.ts +23 -2
- package/src/api/Event.js +38 -11
- package/src/api/Organization.js +129 -45
- package/src/api/Poi.js +38 -12
- package/src/api/Project.js +158 -8
- package/src/api/User.js +273 -18
- package/src/endpoints.module.js +2 -2
- package/src/index.js +21 -1
- package/src/utils/reactive.js +279 -0
package/src/api/BaseEntity.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
80
|
+
/** @type {EndpointApi} */
|
|
67
81
|
this.endpointApi = new deps.EndpointApi(this.apiClient);
|
|
68
82
|
} else if (typeof deps.EndpointApi === "object") {
|
|
69
|
-
/** @type {
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1105
|
-
|
|
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;
|