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