@communecter/cocolight-api-client 1.0.8 → 1.0.10

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,131 +1,325 @@
1
- import { EntityMixin } from "./EntityMixin.js";
2
- import { NewsMixin } from "./NewsMixin.js";
3
- import { UtilMixin } from "./UtilMixin.js";
1
+ import { ApiError } from "../error.js";
2
+ import { DraftStateMixin } from "../mixin/DraftStateMixin.js";
3
+ import { EntityMixin } from "../mixin/EntityMixin.js";
4
+ import { MutualEntityMixin } from "../mixin/MutualEntityMixin.js";
5
+ import { NewsMixin } from "../mixin/NewsMixin.js";
6
+ import { UtilMixin } from "../mixin/UtilMixin.js";
4
7
 
5
- // Project.js
6
8
  export class Project {
7
- // Champs privés pour protéger l'état
8
- #id;
9
- #slug;
10
- #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
+ ]);
11
38
 
12
39
  /**
13
40
  * Crée une instance de Project.
14
- * @param {ApiClient} apiClient - L'instance d'ApiClient.
15
- * @param {Object} identifier - Objet contenant { id } ou { slug }.
16
- */
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.");
17
57
 
18
- constructor(apiClient, { id, slug } = {}) {
19
- if (!id && !slug) {
20
- throw new Error("Vous devez fournir un id ou un slug pour créer un User.");
58
+ if (!parent) throw new ApiError("Parent is required.");
59
+
60
+ this.deps = deps;
61
+
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;
70
+ } else {
71
+ throw new ApiError("Invalid parent for Project.");
21
72
  }
22
- this.apiClient = apiClient;
23
- this.#id = id || null;
24
- this.#slug = slug || null;
73
+
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;
25
92
  }
26
-
27
- // Getters en lecture seule pour chaque propriété
93
+
28
94
  get id() {
29
- return this.#id;
95
+ return this.#draftData.id || null;
30
96
  }
31
97
 
32
98
  _id(newId) {
33
- this.#id = newId;
34
- }
35
-
36
- get slug() {
37
- return this.#slug;
99
+ this.#draftData.id = newId;
38
100
  }
39
101
 
40
- _slug(newSlug) {
41
- this.#slug = newSlug;
42
- }
43
-
44
102
  get isConnected() {
45
103
  return this.apiClient.isConnected;
46
104
  }
47
105
 
48
- get data() {
49
- return this.#data;
106
+ get userId() {
107
+ return this.apiClient.userId;
108
+ }
109
+
110
+ get draftData() {
111
+ return this.#draftData;
50
112
  }
51
113
 
52
- _setData(newData) {
53
- this.#data = newData;
114
+ get initialDraftData() {
115
+ return this.#initialDraftData;
54
116
  }
55
117
 
56
- get userId() {
57
- return this.apiClient.userId;
118
+ get serverData() {
119
+ return this.#serverData;
58
120
  }
59
-
121
+
60
122
  get isMe() {
61
- return this.isConnected && this.userId === this.id;
123
+ return this.isConnected && this.userId === this.parent?.id;
62
124
  }
63
125
 
64
126
  getEntityType() {
65
- return "projects";
127
+ return Project.entityType;
66
128
  }
67
129
 
68
- /**
69
- * Récupère le profil complet de l'organisation.
70
- *
71
- * @returns {Promise<Object>} Le profil complet.
72
- */
73
- async getProfil() {
74
- return this.apiClient.safeCall(async () => {
75
- const data = await this.getPublicProfile();
76
- this._setData(data);
77
- return data;
130
+ _updateInitialDraftSnapshot() {
131
+ this.#initialDraftData = JSON.parse(JSON.stringify(this.#draftData));
132
+ }
133
+
134
+ hasChanges() {
135
+ return JSON.stringify(this.#draftData) !== JSON.stringify(this.#initialDraftData);
136
+ }
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
78
147
  });
148
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft));
149
+ this.#draftData = draft;
150
+ this.data = proxy;
79
151
  }
80
152
 
81
- /**
82
- * Récupérer les actualités : Récupère la liste d’actualités selon plusieurs critères.
83
- * Constant : GET_NEWS
84
- * @param {Object} data - Les données à envoyer.
85
- * @param {number} data.dateLimit - Limite de date timestamp ou 0 (default: 0)
86
- * @param {object} data.search - data.search
87
- * @param {string} data.search.name - Nom ou terme recherché (default: "")
88
- * @param {number} data.indexStep - Nombre de résultats par page (default: 12)
89
- * @returns {Promise<Object>} - Les données de réponse.
90
- * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
91
- * @throws {Error} - En cas d'erreur inattendue.
92
- */
93
- async getNews(data = {}) {
94
- data.pathParams = { type: this.getEntityType(), id: this.id };
95
- return this._getNews(data);
153
+ async refresh() {
154
+ if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.");
155
+ return this.get();
96
156
  }
97
157
 
98
- /**
99
- * Ajouter une actualité : Ajoute une nouvelle actualité.
100
- * Constant : ADD_NEWS
101
- * @param {Object} data - Les données à envoyer.
102
- * @param {string} data.text - Contenu de l’actualité
103
- * @param {string} data.scope - Portée de l'actualité (ex: public, privé...) (default: "public")
104
- * @param {boolean} data.markdownActive - Markdown activé (true/false) (default: true)
105
- * @param {string} data.type - Type de l'objet, toujours 'news'. (default: "news")
106
- * @param {boolean} data.json - Indique que la réponse est au format JSON. (default: true)
107
- * @param {array | string} data.tags - Tags : "" pour effacer tous les tags, ou tableau de mots-clés.
108
- * @param {object} data.mediaImg - Optionnel. Informations sur les images associées à la news.
109
- * @param {number} data.mediaImg.countImages - Nombre d'images.
110
- * @param {Array<string>} data.mediaImg.images - Liste des identifiants ou chemins d'images.
111
- * @param {object} data.mediaFile - Optionnel. Informations sur les fichiers associés à la news.
112
- * @param {number} data.mediaFile.countFiles - Nombre de fichiers.
113
- * @param {Array<string>} data.mediaFile.files - Liste des identifiants ou chemins de fichiers.
114
- * @param {object} data.mentions - Liste des mentions sous forme d'objet avec des clés dynamiques représentant l'indice de la mention.
115
- * @param {Object.<string, object>} data.mentions - Objet dont les clés keys matching ^[0-9]+$
116
- * @returns {Promise<Object>} - Les données de réponse.
117
- * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
118
- * @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
119
- * @throws {Error} - En cas d'erreur inattendue.
120
- */
121
- async addNews(data = {}) {
122
- data.parentId = this.id;
123
- data.parentType = this.getEntityType();
124
- return this.callIsConnected(() => this._addNews(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
+
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
204
+
205
+ // faire ça ou alors resfresh() pour re-synchroniser
206
+ // this.#serverData = data.map;
207
+ }
208
+ }
209
+ }
125
210
  }
211
+
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;
235
+ }
236
+ }
126
237
 
238
+ return hasChanged;
239
+ }
240
+
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));
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;
297
+
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
+ );
307
+ }
308
+ };
309
+
310
+ async project(projectData = {}, ) {
311
+ try {
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();
315
+ }
316
+ return project;
317
+ } catch (error) {
318
+ this.apiClient._logger.error(`[Api.${this.__entityTag}.project] Erreur lors de la création d'une instance project :`, error.message);
319
+ throw error;
320
+ }
321
+ }
322
+
127
323
  }
128
324
 
129
- // Incorporation du mixin dans Project
130
- Object.assign(Project.prototype, EntityMixin, UtilMixin, NewsMixin);
131
-
325
+ Object.assign(Project.prototype, UtilMixin, MutualEntityMixin, EntityMixin, NewsMixin, DraftStateMixin);