@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.
@@ -1,233 +1,315 @@
1
- import { ApiResponseError } from "../error.js";
2
- import { News } from "./News.js";
3
- import { User } from "./User.js";
1
+ import { ApiError } from "../error.js";
2
+ import { DraftStateMixin } from "../mixin/DraftStateMixin.js";
4
3
  import { EntityMixin } from "../mixin/EntityMixin.js";
4
+ import { MutualEntityMixin } from "../mixin/MutualEntityMixin.js";
5
5
  import { NewsMixin } from "../mixin/NewsMixin.js";
6
6
  import { UtilMixin } from "../mixin/UtilMixin.js";
7
7
 
8
8
  // Organization.js
9
9
  export class Organization {
10
- // Champs privés pour protéger l'état
11
- #id;
12
- #slug;
13
- #data = null;
10
+ #draftData = {};
11
+ #initialDraftData = {};
12
+ #serverData = null;
13
+
14
+ static entityType = "organizations";
15
+
16
+ static SCHEMA_CONSTANTS = [
17
+ "ADD_ORGANIZATION",
18
+ "UPDATE_BLOCK_DESCRIPTION",
19
+ "UPDATE_BLOCK_INFO",
20
+ "UPDATE_BLOCK_SOCIAL",
21
+ "UPDATE_BLOCK_LOCALITY",
22
+ "UPDATE_BLOCK_SLUG",
23
+ "PROFIL_IMAGE"
24
+ ];
25
+
26
+ static ADD_BLOCKS = new Map([
27
+ ["ADD_ORGANIZATION", "addOrganization"],
28
+ ["PROFIL_IMAGE", ""]
29
+ ]);
30
+
31
+ static UPDATE_BLOCKS = new Map([
32
+ ["UPDATE_BLOCK_DESCRIPTION", "updateDescription"],
33
+ ["UPDATE_BLOCK_SOCIAL", "updateSocial"],
34
+ ["UPDATE_BLOCK_LOCALITY", "updateLocality"],
35
+ ["UPDATE_BLOCK_INFO", "updateInfo"],
36
+ ["UPDATE_BLOCK_SLUG", "updateSlug"],
37
+ ["PROFIL_IMAGE", "updateImageProfil"]
38
+ ]);
14
39
 
15
40
  /**
16
41
  * Crée une instance de Organization.
17
- * @param {ApiClient} apiClient - L'instance d'ApiClient.
18
- * @param {Object} identifier - Objet contenant { id } ou { slug }.
19
- * @param {Object} [deps={}] - Dépendances injectées (ex: User).
20
- * @param {EndpointApi} deps.EndpointApi - Classe EndpointApi pour éviter les dépendances circulaires.
42
+ *
43
+ * @param {ApiClient|User} parent - Instance de ApiClient ou User.
44
+ * @param {Object} [data={}] - Données de l'organisation.
45
+ * @param {Object} [deps={}] - Dépendances injectées.
46
+ * @param {function|object} deps.EndpointApi - Classe ou instance de EndpointApi pour les appels API.
47
+ * @param {function} deps.User - Classe User pour la gestion des utilisateurs.
48
+ * @param {function} deps.Project - Classe Project pour la gestion des projets.
49
+ * @param {function} deps.News - Classe News pour la gestion des actualités.
50
+ *
51
+ * @throws {ApiError} Si le parent n'est pas valide ou si les dépendances ne sont pas injectées correctement.
21
52
  */
22
53
 
23
- constructor(apiClient, { id, slug } = {}, deps = {}) {
24
- if(!deps.EndpointApi){
25
- throw new Error("EndpointApi class must be injected to avoid circular dependency.");
26
- }
27
- if (!id && !slug) {
28
- throw new Error("Vous devez fournir un id ou un slug pour créer une instance User.");
29
- }
30
- this.apiClient = apiClient;
54
+ constructor(parent, data = {}, deps = {}) {
55
+ this.__entityTag = "Organization";
56
+
57
+ if (!deps.EndpointApi) throw new ApiError("EndpointApi class must be injected to avoid circular dependency.");
58
+ if (!deps.User) throw new ApiError("User class must be injected.");
59
+ if (!deps.Project) throw new ApiError("Project class must be injected.");
60
+ if (!deps.News) throw new ApiError("News class must be injected.");
61
+
62
+ if (!parent) throw new ApiError("Parent is required.");
63
+
31
64
  this.deps = deps;
65
+
66
+ this.EndpointApiClass = deps.EndpointApi;
32
67
 
33
- // Gérer les deux cas : fonction constructeur ou instance
34
- if (typeof deps.EndpointApi === "function") {
35
- this.endpointApi = new deps.EndpointApi(this.apiClient);
36
- } else if (typeof deps.EndpointApi === "object") {
37
- this.endpointApi = deps.EndpointApi;
68
+ if (parent?.__entityTag === "ApiClient") {
69
+ this.apiClient = parent;
70
+ this.parent = null;
71
+ } else if (parent?.__entityTag === "User") {
72
+ this.apiClient = parent.apiClient;
73
+ this.parent = parent;
38
74
  } else {
39
- throw new Error("deps.EndpointApi doit être une classe ou une instance valide.");
75
+ throw new ApiError("Invalid parent for Organization.");
40
76
  }
41
-
42
- this.#id = id || null;
43
- this.#slug = slug || null;
77
+
78
+ this.endpointApi = typeof deps.EndpointApi === "function"
79
+ ? new deps.EndpointApi(this.apiClient)
80
+ : (typeof deps.EndpointApi === "object"
81
+ ? deps.EndpointApi
82
+ : (() => { throw new ApiError("deps.EndpointApi must be a class or instance."); })());
83
+
84
+ this.#serverData = null;
85
+
86
+ const { draft, proxy } = this.buildDraftAndProxy({
87
+ data: { ...data, ...this.defaultFields },
88
+ serverData: this.#serverData,
89
+ constant: Organization.SCHEMA_CONSTANTS,
90
+ apiClient: this.apiClient,
91
+ transforms: this.transforms,
92
+ removeFields: this.removeFields
93
+ });
94
+
95
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft)); // snapshot propre
96
+ this.#draftData = draft;
97
+ this.data = proxy;
44
98
  }
45
99
 
46
100
  // Getters en lecture seule pour chaque propriété
47
101
  get id() {
48
- return this.#id;
102
+ return this.#draftData.id || null;
49
103
  }
50
104
 
51
105
  _id(newId) {
52
- this.#id = newId;
106
+ this.#draftData.id = newId;
53
107
  }
54
108
 
55
109
  get slug() {
56
- return this.#slug;
57
- }
58
-
59
- _slug(newSlug) {
60
- this.#slug = newSlug;
110
+ return this.#draftData.slug || null;
61
111
  }
62
112
 
63
113
  get isConnected() {
64
114
  return this.apiClient.isConnected;
65
115
  }
66
116
 
67
- get data() {
68
- return this.#data;
69
- }
70
-
71
117
  _setData(newData) {
72
- this.#data = newData;
118
+ this.#serverData = { ...newData };
119
+
120
+ const { draft, proxy } = this.buildDraftAndProxy({
121
+ data: { ...newData, ...this.defaultFields },
122
+ serverData: this.#serverData,
123
+ constant: Organization.SCHEMA_CONSTANTS,
124
+ apiClient: this.apiClient,
125
+ transforms: this.transforms,
126
+ removeFields: this.removeFields
127
+ });
128
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft));
129
+ this.#draftData = draft;
130
+ this.data = proxy;
73
131
  }
74
132
 
75
133
  get userId() {
76
134
  return this.apiClient.userId;
77
135
  }
78
-
79
- get isMe() {
80
- return this.isConnected && this.userId === this.id;
81
- }
82
136
 
83
- getEntityType() {
84
- return "organizations";
137
+ get draftData() {
138
+ return this.#draftData;
85
139
  }
86
140
 
87
- /**
88
- * Récupère le profil complet de l'organisation.
89
- *
90
- * @returns {Promise<Object>} Le profil complet.
91
- */
92
- async getProfil() {
93
- return this.apiClient.safeCall(async () => {
94
- const data = await this.getPublicProfile();
95
- this._setData(data);
96
- return data;
97
- });
141
+ get initialDraftData() {
142
+ return this.#initialDraftData;
98
143
  }
99
144
 
100
- /**
101
- * Mettre à jour les paramètres d'un élément : Mise à jour des paramètres spécifiques d'un élément.
102
- * Constant : UPDATE_SETTINGS
103
- * @param {Object} data - Les données à envoyer.
104
- * @param {string} data.type - data.type
105
- * @param {undefined} data.value - data.value
106
- * @returns {Promise<Object>} - Les données de réponse.
107
- * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
108
- * @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
109
- * @throws {Error} - En cas d'erreur inattendue.
110
- */
111
- async updateSettings(data = {}) {
112
- data.idEntity = this.id;
113
- data.typeEntity = this.getEntityType();
114
- return this.callIsConnected(() => this.endpointApi.updateSettings(data));
145
+ get serverData() {
146
+ return this.#serverData;
115
147
  }
116
148
 
117
- /**
118
- * Mettre à jour la description d'un élément : Permet de mettre à jour la description courte et complète d'un élément.
119
- * Constant : UPDATE_BLOCK_DESCRIPTION
120
- * @param {Object} data - Les données à envoyer.
121
- * @param {string} data.descMentions - Mentions dans la description (default: "")
122
- * @param {string} data.shortDescription - Courte description
123
- * @param {string} data.description - Description complète
124
- * @returns {Promise<Object>} - Les données de réponse.
125
- * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
126
- * @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
127
- * @throws {Error} - En cas d'erreur inattendue.
128
- */
129
- async updateDescription(data = {}) {
130
- data.typeElement = this.getEntityType();
131
- data.id = this.id;
132
- return this.callIsConnected(() => this.endpointApi.updateBlockDescription(data));
149
+ get isMe() {
150
+ return this.isConnected && this.userId === this.parent?.id;
133
151
  }
134
-
135
- /**
136
- * Mettre à jour les informations d'un élément : Permet de mettre à jour les informations générales d'un élément (nom, contacts, etc.).
137
- * Constant : UPDATE_BLOCK_INFO
138
- */
139
- async updateInfo(data = {}) {
140
- data.typeElement = this.getEntityType();
141
- data.id = this.id;
142
- return this.callIsConnected(() => this.endpointApi.updateBlockInfo(data));
152
+
153
+ getEntityType() {
154
+ return Organization.entityType;
143
155
  }
156
+
157
+ static fromServerData(data, parent, deps) {
158
+ const instance = new Organization(parent, {}, deps);
159
+ instance.#serverData = { ...data };
160
+
161
+ const { draft, proxy } = instance.buildDraftAndProxy({
162
+ data: { ...data, ...instance.defaultFields },
163
+ serverData: instance.#serverData,
164
+ constant: Organization.SCHEMA_CONSTANTS,
165
+ apiClient: instance.apiClient,
166
+ transforms: instance.transforms,
167
+ removeFields: instance.removeFields
168
+ });
144
169
 
145
- /**
146
- * Mettre à jour les réseaux sociaux d'un élément : Permet de mettre à jour les liens vers les réseaux sociaux d'un élément.
147
- * Constant : UPDATE_BLOCK_SOCIAL
148
- */
149
- async updateSocial(data = {}) {
150
- data.typeElement = this.getEntityType();
151
- data.id = this.id;
152
- return this.callIsConnected(() => this.endpointApi.updateBlockSocial(data));
170
+ instance.#draftData = draft;
171
+ instance.data = proxy;
172
+
173
+ return instance;
153
174
  }
154
-
155
- /**
156
- * Mettre à jour les localités d'un élément : Permet de mettre à jour l'adresse et les informations géographiques d'un élément.
157
- * Constant : UPDATE_BLOCK_LOCALITY
158
- */
159
- async updateLocality(data = {}) {
160
- data.typeElement = this.getEntityType();
161
- data.id = this.id;
162
- return this.callIsConnected(() => this.endpointApi.updateBlockLocality(data));
175
+
176
+ async refresh() {
177
+ if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.");
178
+ return this.get();
163
179
  }
164
-
165
- /**
166
- * Mettre à jour le slug d'un élément : Permet de mettre à jour le slug pour une URL simplifiée.
167
- * Constant : UPDATE_BLOCK_SLUG
168
- */
169
- async updateSlug(slug) {
170
- try {
171
- await this.endpointApi.check({ slug });
172
- } catch (error) {
173
- if(error instanceof ApiResponseError) {
174
- throw new ApiResponseError("Erreur lors de la vérification du slug.", error.status, error.data);
180
+
181
+ async save() {
182
+
183
+ if(!this.isConnected){
184
+ throw new ApiError("Impossible de sauvegarder sans être connecté.");
185
+ }
186
+
187
+ const payload = { ...this.#draftData };
188
+
189
+ if (!this.id) {
190
+ await this._add(payload);
191
+ // this._updateInitialDraftSnapshot();
192
+ await this.refresh();
193
+ return this.#serverData;
194
+ } else {
195
+ const hasChanged = await this._update(payload);
196
+ if (hasChanged) {
197
+ // this._updateInitialDraftSnapshot();
198
+ await this.refresh();
175
199
  }
176
- throw error;
200
+ return this.#serverData;
177
201
  }
178
- const data = { typeElement: this.getEntityType(), id: this.id, slug };
179
- return this.callIsConnected(() => this.endpointApi.updateBlockSlug(data));
202
+
180
203
  }
204
+
205
+ async _add(payload){
206
+ // si pas d'id on le crée
207
+ payload.id = this._newId();
208
+ // on enlève le slug mais il ne devrait pas être là
209
+ if(payload.slug){
210
+ delete payload.slug;
211
+ }
212
+
213
+ for (const [constant, methodName] of Organization.ADD_BLOCKS) {
214
+ const blockData = this.extractChangedFieldsFromSchema(
215
+ this.apiClient,
216
+ constant,
217
+ { ...payload, ...this.defaultFields},
218
+ () => {}
219
+ );
220
+ if (blockData && Object.keys(blockData).length > 0) {
221
+ const data = await this[methodName](blockData);
222
+
223
+ if (!this.id && data?.map?.id) {
224
+ this.#draftData.id = data.map.id;
225
+ this.#draftData.slug = data.map.slug;
226
+ // on met à jour le slug dans le draftData
181
227
 
182
- /**
183
- * Mettre à jour l'image de profil : Permet de mettre à jour l'image de profil d'un utilisateur ou d'une entité.
184
- * Constant : PROFIL_IMAGE
185
- */
186
- async updateImageProfil(image) {
187
- image = await this.validateImage(image);
188
- const data = { pathParams: { folder: this.getEntityType(), ownerId: this.id }, profil_avatar: image };
189
- return this.callIsConnected(() => this.endpointApi.profilImage(data));
228
+ // faire ça ou alors resfresh() pour re-synchroniser
229
+ // this.#serverData = data.map;
230
+ }
231
+ }
232
+ }
190
233
  }
191
234
 
192
- /**
193
- * Récupérer les actualités : Récupère la liste d’actualités selon plusieurs critères.
194
- * Constant : GET_NEWS
195
- * @param {Object} data - Les données à envoyer.
196
- * @param {number} data.dateLimit - Limite de date timestamp ou 0 (default: 0)
197
- * @param {object} data.search - data.search
198
- * @param {string} data.search.name - Nom ou terme recherché (default: "")
199
- * @param {number} data.indexStep - Nombre de résultats par page (default: 12)
200
- * @returns {Promise<Object>} - Les données de réponse.
201
- * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
202
- * @throws {Error} - En cas d'erreur inattendue.
203
- */
204
- async getNews(data = {}) {
205
- data.pathParams = { type: this.getEntityType(), id: this.id };
206
- const arrayObjetNews = await this.endpointApi.getNews(data);
207
- if(!Array.isArray(arrayObjetNews)){
208
- throw new ApiResponseError("Erreur lors de la récupération des actualités.", 500, arrayObjetNews);
235
+ async _update(payload){
236
+ // on enlève l'id car il existe déjà dans les appels API je laisse slug car il peut être changer en update ici
237
+ // if(payload.slug){
238
+ // delete payload.slug;
239
+ // }
240
+ if(payload.id){
241
+ delete payload.id;
209
242
  }
210
- const rawNewsList = arrayObjetNews.map((newsData) =>
211
- News.fromServerData(newsData, this, { User, EndpointApi: this.deps.EndpointApi })
212
- );
213
- return this._createFilteredProxy(rawNewsList);
243
+
244
+ let hasChanged = false;
245
+
246
+
247
+ // Sinon, on fait les updates en blocs
248
+ for (const [constant, methodName] of Organization.UPDATE_BLOCKS) {
249
+ const blockData = this.extractChangedFieldsFromSchema(
250
+ this.apiClient,
251
+ constant,
252
+ { ...payload, ...this.defaultFields},
253
+ () => this.initialDraftData,
254
+ this.removeFields
255
+ );
256
+ if (blockData && Object.keys(blockData).length > 0) {
257
+ await this[methodName](blockData);
258
+ hasChanged = true;
259
+ }
260
+ }
261
+
262
+ return hasChanged;
214
263
  }
215
264
 
216
- async news(newsData) {
265
+ addOrganization(data = {}) {
266
+ return this.callIsConnected(() => this.endpointApi.addOrganization(data));
267
+ }
268
+
269
+ _updateInitialDraftSnapshot() {
270
+ this.#initialDraftData = JSON.parse(JSON.stringify(this.#draftData));
271
+ }
272
+
273
+ hasChanges() {
274
+ return JSON.stringify(this.#draftData) !== JSON.stringify(this.#initialDraftData);
275
+ }
276
+
277
+ defaultFields = {
278
+ typeElement: this.getEntityType()
279
+ };
280
+
281
+ removeFields = [
282
+ "typeElement"
283
+ ];
284
+
285
+ // role = admin c'est bizarre car en faite c'est gérer dans links sur user et l'orga
286
+ // links.members.${this.userId}.isAdmin === true
287
+ transforms = {
288
+ github: (val, full) => full?.socialNetwork?.github,
289
+ gitlab: (val, full) => full?.socialNetwork?.gitlab,
290
+ facebook: (val, full) => full?.socialNetwork?.facebook,
291
+ twitter: (val, full) => full?.socialNetwork?.twitter,
292
+ instagram: (val, full) => full?.socialNetwork?.instagram,
293
+ diaspora: (val, full) => full?.socialNetwork?.diaspora,
294
+ mastodon: (val, full) => full?.socialNetwork?.mastodon,
295
+ telegram: (val, full) => full?.socialNetwork?.telegram,
296
+ signal: (val, full) => full?.socialNetwork?.signal
297
+ };
298
+
299
+ async project(projectData = {}, ) {
217
300
  try {
218
- const news = new News(this, newsData, { User, EndpointApi : this.deps.EndpointApi });
219
- if (newsData.id) {
220
- await news.get();
301
+ const project = new this.deps.Project(this, projectData, { User: this.deps.User, News: this.deps.News, EndpointApi : this.deps.EndpointApi });
302
+ if (projectData.id || projectData.slug) {
303
+ await project.get();
221
304
  }
222
- return news;
305
+ return project;
223
306
  } catch (error) {
224
- console.error("[Api.project.news] Erreur lors de la création d'une instance news :", error.message);
307
+ this.apiClient._logger.error(`[Api.${this.__entityTag}.project] Erreur lors de la création d'une instance project :`, error.message);
225
308
  throw error;
226
309
  }
227
310
  }
228
-
311
+
229
312
  }
230
313
 
231
314
  // Incorporation du mixin dans Organization
232
- Object.assign(Organization.prototype, EntityMixin, UtilMixin, NewsMixin);
233
-
315
+ Object.assign(Organization.prototype, UtilMixin, MutualEntityMixin, EntityMixin, NewsMixin, DraftStateMixin);