@communecter/cocolight-api-client 1.0.9 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@communecter/cocolight-api-client",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,7 +32,8 @@
32
32
  "generate:testdata": "node ./scripts/generate-test-data.js",
33
33
  "generate:reponses": "node ./scripts/generate-constant-response-200.js",
34
34
  "generate:methodeapi": "node ./scripts/generate-methode-api.js",
35
- "generate:ajv-standalone": "node ./scripts/generate-validate-function-ajv.js"
35
+ "generate:ajv-standalone": "node ./scripts/generate-validate-function-ajv.js",
36
+ "generate:entities": "node scripts/generate-entities.js"
36
37
  },
37
38
  "keywords": [
38
39
  "communecter",
package/src/Api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // Api.js
2
2
  import EndpointApi from "./api/EndpointApi.js";
3
+ import { News } from "./api/News.js";
3
4
  import { Organization } from "./api/Organization.js";
4
5
  import { Project } from "./api/Project.js";
5
6
  import { User } from "./api/User.js";
@@ -76,12 +77,14 @@ export default class Api {
76
77
  /**
77
78
  * Retourne l'utilisateur connecté.
78
79
  *
79
- * @returns {User} L'utilisateur connecté.
80
+ * @returns {Promise<User>} L'utilisateur connecté.
81
+ * @throws {ApiAuthenticationError} Si l'utilisateur n'est pas authentifié.
80
82
  */
81
- me() {
83
+ async me() {
82
84
  if (!this._loggedUser) {
83
85
  throw new ApiAuthenticationError("Accès refusé : utilisateur non authentifié.");
84
86
  }
87
+ await this._loggedUser.get();
85
88
  return this._loggedUser;
86
89
  }
87
90
 
@@ -89,11 +92,18 @@ export default class Api {
89
92
  * Crée une instance User pour un utilisateur donné (autre que le connecté).
90
93
  *
91
94
  * @param {Object} userData - Les données de l'utilisateur public.
92
- * @returns {User}
95
+ * @returns {Promise<User>} Une promesse qui résout l'instance User.
96
+ * @throws {Error} Si une erreur se produit lors de la création de l'utilisateur.
93
97
  */
94
- user(userData) {
98
+ async user(userData) {
95
99
  try {
96
- return new User(this._client, userData, {}, { EndpointApi });
100
+ if (!userData.id && !userData.slug) {
101
+ return new User(this._client, userData, { EndpointApi, Organization, Project, News });
102
+ } else {
103
+ const user = new User(this._client, userData, { EndpointApi, Organization, Project, News });
104
+ await user.get();
105
+ return user;
106
+ }
97
107
  } catch (error) {
98
108
  console.error("[Api.user] Erreur lors de la création d'un objet utilisateur public :", error.message);
99
109
  throw error;
@@ -105,18 +115,17 @@ export default class Api {
105
115
  * Creates an Organization object and optionally retrieves its profile.
106
116
  *
107
117
  * @param {Object} organizationData - The data required to initialize the Organization object.
108
- * @param {Object} [options={ getProfile: true }] - Additional options for the organization creation.
109
- * @param {boolean} [options.getProfile=true] - Whether to fetch the organization's profile after creation.
110
118
  * @returns {Promise<Organization>} A promise that resolves to the created Organization object.
111
119
  * @throws {Error} Throws an error if the organization creation or profile retrieval fails.
112
120
  */
113
- async organization(organizationData, options = { getProfile: true }) {
121
+ async organization(organizationData) {
114
122
  try {
115
- const oraganization = new Organization(this._client, organizationData, { EndpointApi });
116
- if (options.getProfile) {
117
- await oraganization.getProfil();
123
+ const organization = new Organization(this._client, organizationData, { EndpointApi, User, Project, News });
124
+ if (!organizationData.id && !organizationData.slug) {
125
+ throw new Error("Vous devez fournir un id ou un slug pour créer une instance Organization.");
118
126
  }
119
- return oraganization;
127
+ await organization.get();
128
+ return organization;
120
129
  } catch (error) {
121
130
  console.error("[Api.organization] Erreur lors de la création d'un objet organisation :", error.message);
122
131
  throw error;
@@ -127,17 +136,16 @@ export default class Api {
127
136
  * Creates a new Project instance and optionally retrieves its profile.
128
137
  *
129
138
  * @param {Object} projectData - The data used to initialize the Project instance.
130
- * @param {Object} [options={ getProfile: true }] - Additional options for project creation.
131
- * @param {boolean} [options.getProfile=true] - Whether to retrieve the project's profile after creation.
132
139
  * @returns {Promise<Project>} A promise that resolves to the created Project instance.
133
140
  * @throws {Error} If an error occurs during project creation or profile retrieval.
134
141
  */
135
- async project(projectData, options = { getProfile: true }) {
142
+ async project(projectData) {
136
143
  try {
137
- const project = new Project(this._client, projectData, { EndpointApi });
138
- if (options.getProfile) {
139
- await project.getProfil();
144
+ const project = new Project(this._client, projectData, { User, News, EndpointApi });
145
+ if (!projectData.id && !projectData.slug) {
146
+ throw new Error("Vous devez fournir un id ou un slug pour créer une instance Project.");
140
147
  }
148
+ await project.get();
141
149
  return project;
142
150
  } catch (error) {
143
151
  console.error("[Api.project] Erreur lors de la création d'un objet projet :", error.message);
@@ -150,11 +158,15 @@ export default class Api {
150
158
  *
151
159
  * @returns {ApiClient}
152
160
  */
153
-
154
161
  get client() {
155
162
  return this._client;
156
163
  }
157
164
 
165
+ /**
166
+ * Retourne l'instance d'EndpointApi.
167
+ *
168
+ * @returns {EndpointApi}
169
+ */
158
170
  get endpointApi() {
159
171
  return new EndpointApi(this._client);
160
172
  }
package/src/ApiClient.js CHANGED
@@ -51,6 +51,8 @@ export default class ApiClient extends EventEmitter {
51
51
  throw new ApiClientError("Le paramètre \"baseURL\" est obligatoire.", 500);
52
52
  }
53
53
 
54
+ this.__entityTag = "ApiClient";
55
+
54
56
  this._baseURL = baseURL;
55
57
  this._refreshToken = refreshToken;
56
58
  this._refreshUrl = refreshUrl;
@@ -86,7 +88,6 @@ export default class ApiClient extends EventEmitter {
86
88
  });
87
89
 
88
90
  // Pino logger
89
- // (Ici en mode pretty-print sur la console, tu peux configurer comme tu veux)
90
91
  this._logger = pino({
91
92
  transport: {
92
93
  target: "pino-pretty",
@@ -1379,5 +1380,13 @@ export default class ApiClient extends EventEmitter {
1379
1380
  };
1380
1381
 
1381
1382
 
1383
+ getRequestSchema(constant) {
1384
+ const endpoint = this._endpoints.find(e => e.constant === constant);
1385
+ return endpoint?.request || null;
1386
+ }
1387
+
1388
+ getPathSchema(constant) {
1389
+ return this._endpoints.find(e => e.constant === constant)?.pathParams || null;
1390
+ }
1382
1391
 
1383
1392
  }
package/src/api/News.js CHANGED
@@ -1,51 +1,70 @@
1
1
  import { ApiError } from "../error.js";
2
- import { NewsMixin } from "../mixin/NewsMixin.js";
2
+ import { DraftStateMixin } from "../mixin/DraftStateMixin.js";
3
3
  import { UtilMixin } from "../mixin/UtilMixin.js";
4
4
 
5
5
  // News.js
6
6
  export class News {
7
- // Champs privés pour protéger l'état
8
7
  #draftData = {};
9
- // Data d'une news venant du serveur
8
+ #initialDraftData = {};
10
9
  #serverData = null;
11
10
 
11
+ static entityType = "news";
12
+
13
+ static SCHEMA_CONSTANTS = [
14
+ "ADD_NEWS"
15
+ ];
16
+
12
17
  /**
13
18
  * Crée une instance de News.
14
19
  * @param {(Organization|Project|User)} parent - L'instance de l'element parent (ex: Organization, Project, User).
15
- * @param {Object} identifier - Objet contenant { id }
20
+ * @param {Object} data - Objet contenant { id }
16
21
  * @param {Object} deps - Dépendances injectées (ex: User).
17
22
  * @param {EndpointApi} deps.EndpointApi - Classe EndpointApi pour éviter les dépendances circulaires.
18
23
  * @param {User} deps.User - Classe User pour éviter les dépendances circulaires.
19
24
  */
20
25
 
21
26
  constructor(parent, data = {}, deps = {}) {
22
- if(!deps.EndpointApi){
23
- throw new ApiError("EndpointApi class must be injected to avoid circular dependency.");
24
- }
25
- if (!deps.User) {
26
- throw new ApiError("User class must be injected to avoid circular dependency.");
27
- }
28
- if (!parent) {
29
- throw new ApiError("Parent is required.");
30
- }
27
+ this.__entityTag = "News";
28
+
29
+ if (!deps.EndpointApi) throw new ApiError("EndpointApi class must be injected to avoid circular dependency.");
30
+ if (!deps.User) throw new ApiError("User class must be injected.");
31
+
32
+ if (!parent) throw new ApiError("Parent is required.");
31
33
  this.deps = deps;
32
- this.UserClass = deps.User;
33
- this.apiClient = parent.apiClient;
34
-
35
- // Gérer les deux cas : fonction constructeur ou instance
36
- if (typeof deps.EndpointApi === "function") {
37
- this.endpointApi = new deps.EndpointApi(this.apiClient);
38
- } else if (typeof deps.EndpointApi === "object") {
39
- this.endpointApi = deps.EndpointApi;
34
+ this.EndpointApiClass = deps.EndpointApi;
35
+
36
+ if (parent?.__entityTag === "User" || parent?.__entityTag === "Organization" || parent?.__entityTag === "Project") {
37
+ this.apiClient = parent.apiClient;
38
+ this.parent = parent;
40
39
  } else {
41
- throw new ApiError("deps.EndpointApi doit être une classe ou une instance valide.");
40
+ throw new ApiError("Invalid parent for News.");
42
41
  }
42
+
43
+ this.endpointApi = typeof deps.EndpointApi === "function"
44
+ ? new deps.EndpointApi(this.apiClient)
45
+ : (typeof deps.EndpointApi === "object"
46
+ ? deps.EndpointApi
47
+ : (() => { throw new ApiError("deps.EndpointApi must be a class or instance."); })());
48
+
49
+
50
+ this.#serverData = null;
43
51
 
44
- this.#draftData = { ...data };
45
- this.parent = parent;
52
+ const { draft, proxy } = this.buildDraftAndProxy({
53
+ data: { ...data, ...this.defaultFields },
54
+ serverData: this.#serverData,
55
+ constant: News.SCHEMA_CONSTANTS,
56
+ apiClient: this.apiClient,
57
+ transforms: this.transforms,
58
+ removeFields: this.removeFields
59
+ });
60
+
61
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft)); // snapshot propre
62
+ this.#draftData = draft;
63
+ this.data = proxy;
46
64
 
47
65
  }
48
66
 
67
+
49
68
  // Getters en lecture seule pour chaque propriété
50
69
  get id() {
51
70
  return this.#draftData.id || null;
@@ -58,6 +77,10 @@ export class News {
58
77
  get draftData() {
59
78
  return this.#draftData;
60
79
  }
80
+
81
+ get initialDraftData() {
82
+ return this.#initialDraftData;
83
+ }
61
84
 
62
85
  get serverData() {
63
86
  return this.#serverData;
@@ -71,18 +94,41 @@ export class News {
71
94
  return this.isConnected && this.userId === this.parent.id;
72
95
  }
73
96
 
97
+ _setData(newData) {
98
+ this.#serverData = { ...newData };
99
+
100
+ const { draft, proxy } = this.buildDraftAndProxy({
101
+ data: { ...newData, ...this.defaultFields },
102
+ serverData: this.#serverData,
103
+ constant: News.SCHEMA_CONSTANTS,
104
+ apiClient: this.apiClient,
105
+ transforms: this.transforms,
106
+ removeFields: this.removeFields
107
+ });
108
+ this.#initialDraftData = JSON.parse(JSON.stringify(draft));
109
+ this.#draftData = draft;
110
+ this.data = proxy;
111
+ }
112
+
74
113
  getEntityType() {
75
- return "news";
114
+ return News.entityType;
76
115
  }
77
116
 
78
117
  static fromServerData(data, parent, deps) {
79
- const instance = new News(parent, { id: data.id }, deps);
80
-
81
- // Injecte les données serveur
118
+ const instance = new News(parent, {}, deps);
82
119
  instance.#serverData = { ...data };
83
120
 
84
- // Construit les données draft à partir du serverData
85
- instance.#draftData = instance._getDraftFromServerData(data);
121
+ const { draft, proxy } = instance.buildDraftAndProxy({
122
+ data: { ...data, ...instance.defaultFields },
123
+ serverData: instance.#serverData,
124
+ constant: News.SCHEMA_CONSTANTS,
125
+ apiClient: instance.apiClient,
126
+ transforms: instance.transforms,
127
+ removeFields: instance.removeFields
128
+ });
129
+
130
+ instance.#draftData = draft;
131
+ instance.data = proxy;
86
132
 
87
133
  return instance;
88
134
  }
@@ -97,64 +143,23 @@ export class News {
97
143
  * Constant : GET_NEWS_BY_ID
98
144
  */
99
145
  async get() {
100
- if(!this.id) {
101
- throw new ApiError("Vous devez fournir un id pour récupérer une news.");
102
- }
103
- const newsArray = await this.callIsConnected(() => this.endpointApi.getNewsById({ ids: [this.id] }));
104
- if (newsArray && newsArray.length > 0) {
105
- this.#serverData = newsArray[0];
106
- this.#draftData = this._getDraftFromServerData(this.#serverData);
146
+ if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.");
147
+
148
+ const newsArray = await this.callIsConnected(() =>
149
+ this.endpointApi.getNewsById({ ids: [this.id] })
150
+ );
151
+
152
+ if (newsArray && Array.isArray(newsArray) && newsArray.length === 1) {
153
+ const data = newsArray[0];
154
+ this.#serverData = { ...data };
155
+
156
+ this._setData(data);
157
+
107
158
  return this.#serverData;
108
159
  }
109
160
  throw new ApiError(`Aucune actualité trouvée pour l'ID ${this.id}`);
110
161
  }
111
162
 
112
- _getDraftFromServerData(data) {
113
- // je veux copier que certaine info si présente dans le draftData
114
- // - newsArray[0].scope.type > draftData.scope
115
- // - newsArray[0].text > draftData.text
116
- // - newsArray[0].tags > draftData.tags
117
- // - newsArray[0].mentions > draftData.mentions > draftData.mentions.count integer
118
- // - newsArray[0].mediaImg > draftData.mediaImg > draftData.mediaImg.images > un array id
119
- // - newsArray[0].mediaFile > draftData.mediaFile > draftData.mediaFile.files > un array id
120
- const transformed = this._pickProps(
121
- data,
122
- ["scope", "text", "tags", "mentions", "mediaImg", "mediaFile"],
123
- {
124
- scope: (val) => val?.type,
125
- mentions: (val) =>
126
- Array.isArray(val)
127
- ? val.map((mention) => ({
128
- ...mention,
129
- count: parseInt(mention.count)
130
- }))
131
- : [],
132
- mediaImg: (val) => {
133
- const images = val?.images?.map((img) => img.id).filter(Boolean) || [];
134
- return images.length > 0
135
- ? {
136
- countImages: parseInt(val.countImages) || images.length,
137
- images
138
- }
139
- : undefined;
140
- },
141
- mediaFile: (val) => {
142
- const files = val?.files?.map((file) => file.id).filter(Boolean) || [];
143
- return files.length > 0
144
- ? {
145
- countFiles: parseInt(val.countFiles) || files.length,
146
- files
147
- }
148
- : undefined;
149
- }
150
- }
151
- );
152
-
153
- return {
154
- id: data.id,
155
- ...Object.fromEntries(Object.entries(transformed).filter(([, v]) => v !== undefined))
156
- };
157
- }
158
163
 
159
164
  /**
160
165
  * Ajouter une actualité : Ajoute une nouvelle actualité.
@@ -162,6 +167,10 @@ export class News {
162
167
  */
163
168
  async save() {
164
169
 
170
+ if(!this.isConnected){
171
+ throw new ApiError("Impossible de sauvegarder sans être connecté.");
172
+ }
173
+
165
174
  // si le texte est vide, on met un espace pour que le champ soit pris en compte car sur le serveur
166
175
  // il y a une vérification de la taille du texte je pense
167
176
  if(this.#draftData?.text === ""){
@@ -203,8 +212,8 @@ export class News {
203
212
  if (!slug && !id) {
204
213
  throw new ApiError("Vous devez fournir un slug ou un id pour ajouter une mention.");
205
214
  }
206
- const userInstance = new this.UserClass(this.apiClient, { id, slug }, null , { EndpointApi: this.endpointApi });
207
- const user = await userInstance.getProfil();
215
+ const userInstance = new this.deps.User(this.apiClient, { id, slug }, { EndpointApi: this.endpointApi });
216
+ const user = userInstance.serverData;
208
217
  if (!this.#draftData.mentions) {
209
218
  this.#draftData.mentions = [];
210
219
  }
@@ -229,6 +238,7 @@ export class News {
229
238
 
230
239
  }
231
240
 
241
+
232
242
  /**
233
243
  * Ajouter une image à une actualité : Ajoute une images à une actualité.
234
244
  * Constant : ADD_IMAGE_NEWS
@@ -278,9 +288,40 @@ export class News {
278
288
  this.#draftData.id = null;
279
289
  this._isDeleted = true;
280
290
  }
281
-
291
+
292
+ _updateInitialDraftSnapshot() {
293
+ this.#initialDraftData = JSON.parse(JSON.stringify(this.#draftData));
294
+ }
295
+
296
+ hasChanges() {
297
+ return JSON.stringify(this.#draftData) !== JSON.stringify(this.#initialDraftData);
298
+ }
299
+
300
+ defaultFields = {
301
+
302
+ };
303
+
304
+ removeFields = [
305
+
306
+ ];
307
+
308
+ transforms = {
309
+ scope: val => val?.type,
310
+ mentions: val =>
311
+ Array.isArray(val)
312
+ ? val.map(m => ({ ...m, count: parseInt(m.count) }))
313
+ : [],
314
+ mediaImg: val => {
315
+ const images = val?.images?.map(img => img.id).filter(Boolean) || [];
316
+ return images.length > 0 ? { countImages: images.length, images } : undefined;
317
+ },
318
+ mediaFile: val => {
319
+ const files = val?.files?.map(f => f.id).filter(Boolean) || [];
320
+ return files.length > 0 ? { countFiles: files.length, files } : undefined;
321
+ }
322
+ };
323
+
282
324
  }
283
325
 
284
326
  // Incorporation du mixin dans Organization
285
- Object.assign(News.prototype, UtilMixin, NewsMixin);
286
-
327
+ Object.assign(News.prototype, UtilMixin, DraftStateMixin);