@communecter/cocolight-api-client 1.0.40 → 1.0.42

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.40",
3
+ "version": "1.0.42",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5,7 +5,7 @@ import EJSON from "ejson";
5
5
  import pkg from "file-type";
6
6
 
7
7
  import { ApiAuthenticationError, ApiError, ApiResponseError, ApiValidationError } from "../error.js";
8
- import { autoSyncDraftFromSchema, reactive } from "../utils/reactive.js";
8
+ import { isReactive, isSignal, reactive } from "../utils/reactive.js";
9
9
  const { fromBuffer } = pkg;
10
10
 
11
11
  /**
@@ -89,9 +89,9 @@ export class BaseEntity {
89
89
  throw new ApiError("deps.EndpointApi doit être une classe ou une instance valide.");
90
90
  }
91
91
 
92
- this._serverData = null;
92
+ this._serverData = reactive({});
93
93
 
94
- const { draft, proxy } = this._buildDraftAndProxy({
94
+ const { draft, proxy, initial } = this._buildDraftAndProxy({
95
95
  data: { ...data, ...this.defaultFields },
96
96
  serverData: this._serverData,
97
97
  constant: this.constructor.SCHEMA_CONSTANTS,
@@ -100,7 +100,7 @@ export class BaseEntity {
100
100
  removeFields: this.removeFields
101
101
  });
102
102
 
103
- this._initialDraftData = JSON.parse(JSON.stringify(draft));
103
+ this._initialDraftData = initial;
104
104
  this._draftData = draft;
105
105
  this.data = proxy;
106
106
  }
@@ -162,7 +162,7 @@ export class BaseEntity {
162
162
  * @returns {boolean}
163
163
  */
164
164
  hasChanges() {
165
- return JSON.stringify(this._draftData) !== JSON.stringify(this._initialDraftData);
165
+ return this._serialize(this._toRawDeep(this._draftData)) !== this._serialize(this._initialDraftData);
166
166
  }
167
167
 
168
168
  /**
@@ -186,6 +186,7 @@ export class BaseEntity {
186
186
 
187
187
  if (!this.id && typeof this._add === "function") {
188
188
  await this._add(payload);
189
+ this._resetInitialDraftData();
189
190
  // on refresh le contexte utilisateur si besoin
190
191
  if(this.userContext) {
191
192
  await this.userContext.refresh();
@@ -193,6 +194,7 @@ export class BaseEntity {
193
194
  return await this.refresh();
194
195
  } else if (typeof this._update === "function") {
195
196
  const hasChanged = await this._update(payload);
197
+ this._resetInitialDraftData();
196
198
  if (hasChanged) return await this.refresh();
197
199
  }
198
200
 
@@ -223,29 +225,94 @@ export class BaseEntity {
223
225
  * @returns {void}
224
226
  * @private
225
227
  */
226
- _setData(newData) {
228
+ _setData(newData, { forceInitialDraftReset = false } = {}) {
227
229
  if (this.userContext && this.userContext !== this) {
228
230
  this.apiClient._logger?.info?.(`[${this.__entityTag}] Mise à jour liée à userContext : ${this.userContext.id}`);
229
231
  }
230
- this._serverData = reactive({ ...newData });
231
232
 
232
- const { draft, proxy } = this._buildDraftAndProxy({
233
- data: { ...newData, ...this.defaultFields },
233
+ if (isReactive(this._serverData)) {
234
+ Object.assign(this._serverData, newData);
235
+ } else {
236
+ this._serverData = reactive({ ...newData });
237
+ }
238
+
239
+ const clientDraft = this._draftData ? this._toRawDeep(this._draftData) : {};
240
+
241
+ const mergedData = {
242
+ ...newData,
243
+ ...this.defaultFields,
244
+ ...clientDraft
245
+ };
246
+
247
+ const { draft, proxy, initial } = this._buildDraftAndProxy({
248
+ data: mergedData,
234
249
  serverData: this._serverData,
250
+ previousDraft: this._draftData,
235
251
  constant: this.constructor.SCHEMA_CONSTANTS,
236
252
  apiClient: this.apiClient,
237
253
  transforms: this.transforms,
238
254
  removeFields: this.removeFields
239
255
  });
240
- this._initialDraftData = JSON.parse(JSON.stringify(draft));
241
- this._draftData = draft;
242
- this.data = proxy;
243
256
 
244
- if (this._syncReactiveDraft) {
245
- this.setupReactiveSync();
257
+ if (forceInitialDraftReset) {
258
+ this._initialDraftData = structuredClone(this._toRawDeep(draft));
259
+ } else if (!this._initialDraftData) {
260
+ this._initialDraftData = initial;
261
+ }
262
+
263
+ if (isReactive(this._draftData)) {
264
+ this._updateDraftPreservingUserChanges(draft);
265
+ } else {
266
+ this._draftData = reactive(draft);
267
+ }
268
+
269
+ if (!this.data) {
270
+ this.data = proxy;
271
+ }
272
+
273
+ }
274
+
275
+ _updateDraftPreservingUserChanges(draft) {
276
+ for (const key of Object.keys(draft)) {
277
+ const current = this._draftData?.[key];
278
+ const initialValue = this._initialDraftData?.[key];
279
+
280
+ const isModified =
281
+ current !== undefined &&
282
+ initialValue !== undefined &&
283
+ this._serialize(this._toRawDeep(current)) !== this._serialize(initialValue);
284
+
285
+ if (!isModified) {
286
+ this._draftData[key] = draft[key];
287
+ }
246
288
  }
247
289
  }
248
290
 
291
+ _resetInitialDraftData() {
292
+ const raw = this._toRawDeep(this._draftData);
293
+ this._initialDraftData = structuredClone(raw);
294
+ }
295
+
296
+
297
+ _toRawDeep(obj) {
298
+ if (isSignal(obj)) {
299
+ return this._toRawDeep(obj.value);
300
+ }
301
+
302
+ if (Array.isArray(obj)) {
303
+ return obj.map(this._toRawDeep);
304
+ }
305
+
306
+ if (typeof obj === "object" && obj !== null) {
307
+ const result = {};
308
+ for (const key of Object.keys(obj)) {
309
+ result[key] = this._toRawDeep(obj[key]);
310
+ }
311
+ return result;
312
+ }
313
+
314
+ return obj; // valeur primitive
315
+ }
249
316
 
250
317
  /**
251
318
  * Champs à ajouter automatiquement à chaque draft (ex: `typeElement`).
@@ -768,9 +835,24 @@ export class BaseEntity {
768
835
  _createDraftProxy(apiClient, server = {}, draft = {}, allowedFields = [], transforms = {}, options = {}) {
769
836
  return new Proxy({}, {
770
837
  get: (_, prop) => {
771
- const val = prop in draft ? draft[prop] : server[prop];
772
- const transformer = transforms[prop];
773
- return typeof transformer === "function" ? transformer(val) : val;
838
+ // Ne pas tenter d’accéder à des propriétés système
839
+ if (typeof prop !== "string" && typeof prop !== "symbol") return undefined;
840
+
841
+ // Lecture explicite — déclenche signal si draft est réactif
842
+ if (prop in draft) {
843
+ const value = draft[prop];
844
+ const transformer = transforms[prop];
845
+ return typeof transformer === "function" ? transformer(value) : value;
846
+ }
847
+
848
+ // Fallback vers serverData — aussi potentiellement réactif
849
+ if (server && typeof server === "object" && prop in server) {
850
+ const value = server[prop];
851
+ const transformer = transforms[prop];
852
+ return typeof transformer === "function" ? transformer(value) : value;
853
+ }
854
+
855
+ return undefined;
774
856
  },
775
857
 
776
858
  set: (_, prop, value) => {
@@ -786,7 +868,13 @@ export class BaseEntity {
786
868
  apiClient._logger.warn(message);
787
869
  return false;
788
870
  }
789
- draft[prop] = value;
871
+ const current = draft[prop];
872
+
873
+ if (current && typeof current === "object" && current.__isSignal === true) {
874
+ current.value = value; // ✅ met à jour le signal
875
+ } else {
876
+ draft[prop] = value; // fallback classique
877
+ }
790
878
  return true;
791
879
  },
792
880
 
@@ -797,8 +885,16 @@ export class BaseEntity {
797
885
  },
798
886
 
799
887
  has: (_, prop) => prop in draft || prop in server,
800
- ownKeys: () => [...new Set([...Object.keys(server), ...Object.keys(draft)])],
801
- getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true })
888
+
889
+ ownKeys: () => [...new Set([
890
+ ...Object.keys(server || {}),
891
+ ...Object.keys(draft || {})
892
+ ])],
893
+
894
+ getOwnPropertyDescriptor: () => ({
895
+ enumerable: true,
896
+ configurable: true
897
+ })
802
898
  });
803
899
  }
804
900
 
@@ -874,7 +970,7 @@ export class BaseEntity {
874
970
  * @returns {Object} - Objet contenant le brouillon et le proxy.
875
971
  * @private
876
972
  */
877
- _buildDraftAndProxy({ data = {}, serverData = null, constant, apiClient, transforms = {}, throwOnError = true, removeFields = [] }) {
973
+ _buildDraftAndProxy({ data = {}, serverData = null, previousDraft = null, constant, apiClient, transforms = {}, throwOnError = true, removeFields = [] }) {
878
974
  const constants = Array.isArray(constant) ? constant : [constant];
879
975
  const combinedSchema = {
880
976
  allOf: [],
@@ -898,7 +994,7 @@ export class BaseEntity {
898
994
 
899
995
  combinedSchema.allOf.push(sch);
900
996
  }
901
- const draft = {};
997
+
902
998
  let allowed = this._extractWritableFields(combinedSchema, data);
903
999
 
904
1000
  if (data.id && allowed.indexOf("id") === -1) {
@@ -910,15 +1006,32 @@ export class BaseEntity {
910
1006
 
911
1007
  allowed = allowed.filter(k => !removeFields.includes(k));
912
1008
 
913
- for (const key of allowed) {
914
- const raw = data[key];
915
- const transformed = typeof transforms[key] === "function" ? transforms[key](raw, data) : raw;
916
- if (transformed !== undefined) draft[key] = transformed;
917
- }
1009
+ // Transformation des champs autorisés
1010
+ const rawDraft = Object.fromEntries(
1011
+ allowed.map((key) => {
1012
+ const raw = data[key];
1013
+ const transformed = typeof transforms[key] === "function"
1014
+ ? transforms[key](raw, data)
1015
+ : raw;
1016
+ return [key, transformed];
1017
+ // eslint-disable-next-line no-unused-vars
1018
+ }).filter(([_, v]) => v !== undefined)
1019
+ );
918
1020
 
919
- const proxy = this._createDraftProxy(apiClient, serverData, draft, allowed, transforms, { throwOnError });
1021
+ const initial = structuredClone ? structuredClone(rawDraft) : JSON.parse(JSON.stringify(rawDraft));
920
1022
 
921
- return { draft, proxy };
1023
+ const draft = isReactive(previousDraft)
1024
+ ? Object.assign(previousDraft, rawDraft)
1025
+ : reactive(rawDraft);
1026
+
1027
+ // Assure que serverData est réactif si c'est un objet
1028
+ const reactiveServer = isReactive(serverData)
1029
+ ? serverData
1030
+ : (serverData && typeof serverData === "object" ? reactive(serverData) : serverData);
1031
+
1032
+ const proxy = this._createDraftProxy(apiClient, reactiveServer, draft, allowed, transforms, { throwOnError });
1033
+
1034
+ return { draft, proxy, initial};
922
1035
  }
923
1036
 
924
1037
  /**
@@ -962,25 +1075,6 @@ export class BaseEntity {
962
1075
  return Object.keys(changed).length > 0 ? changed : null;
963
1076
  }
964
1077
 
965
- /**
966
- * ───────────────────────────────
967
- * ReactiveMixin
968
- * ───────────────────────────────
969
- */
970
-
971
- /**
972
- * Active la synchronisation réactive entre le brouillon et le schéma.
973
- *
974
- * @returns {void}
975
- * @public
976
- */
977
- setupReactiveSync() {
978
- if (!this._syncReactiveDraft) {
979
- this._syncReactiveDraft = true;
980
- }
981
- autoSyncDraftFromSchema(this);
982
- }
983
-
984
1078
  /**
985
1079
  * ───────────────────────────────
986
1080
  * MutualEntityMixin
@@ -1594,7 +1688,7 @@ export class BaseEntity {
1594
1688
  async get() {
1595
1689
  return this.apiClient.safeCall(async () => {
1596
1690
  const data = await this._getPublicProfile();
1597
- this._setData(data);
1691
+ this._setData(data, { forceInitialDraftReset: true });
1598
1692
  return data;
1599
1693
  });
1600
1694
  }
@@ -1,7 +1,5 @@
1
1
  // reactive.js - Système maison combinant Signal + Proxy pour réactivité profonde et fine
2
2
 
3
- import { ApiError } from "../error.js";
4
-
5
3
  const effectStack = [];
6
4
  const computedCache = new WeakMap();
7
5
  const signalRegistry = new WeakMap();
@@ -56,7 +54,12 @@ function createSignal(initialValue) {
56
54
  return signal;
57
55
  }
58
56
 
59
- function isSignal(obj) {
57
+ /**
58
+ * vérifie si un objet est un signal.
59
+ * @param {*} obj
60
+ * @returns {boolean}
61
+ */
62
+ export function isSignal(obj) {
60
63
  return obj && obj.__isSignal === true;
61
64
  }
62
65
 
@@ -199,95 +202,3 @@ export function subscribeTo(obj, key, callback) {
199
202
  const signal = signals.get(key);
200
203
  return signal?.subscribe?.(() => callback(signal.value)) ?? (() => {});
201
204
  }
202
-
203
- // --- Helpers internes ---
204
- function _getPathValue(obj, path) {
205
- return path.split(".").reduce((o, k) => (o ? o[k] : undefined), obj);
206
- }
207
-
208
- function _setPathValue(obj, path, value) {
209
- const keys = path.split(".");
210
- const lastKey = keys.pop();
211
- const parent = keys.reduce((o, k) => {
212
- if (o[k] == null) o[k] = {};
213
- return o[k];
214
- }, obj);
215
- parent[lastKey] = value;
216
- }
217
-
218
- function _deletePathValue(obj, path) {
219
- const keys = path.split(".");
220
- const lastKey = keys.pop();
221
- const parent = keys.reduce((o, k) => o?.[k], obj);
222
- if (parent && lastKey in parent) delete parent[lastKey];
223
- }
224
-
225
- function _applyTransformIfExists(path, val, entity) {
226
- const topKey = path.split(".")[0];
227
- const transform = entity.transforms?.[topKey];
228
- return typeof transform === "function" ? transform(val, entity.serverData) : val;
229
- }
230
-
231
- /**
232
- * Synchronise certaines clés du draft avec serverData tant qu'elles ne sont pas modifiées.
233
- * @param {object} entityInstance
234
- * @param {string[]} keys
235
- * @param {{ cleanIfEqual?: boolean }} [options]
236
- */
237
- export function syncDraftOnServerChange(entityInstance, keys = [], { cleanIfEqual = true } = {}) {
238
- for (const path of keys) {
239
- effect(() => {
240
- const rawServerVal = _getPathValue(entityInstance.serverData, path);
241
- const transformedServerVal = _applyTransformIfExists(path, rawServerVal, entityInstance);
242
-
243
- const draftVal = _getPathValue(entityInstance.draftData, path);
244
- const initialVal = _getPathValue(entityInstance.initialDraftData, path);
245
-
246
- const isModified = JSON.stringify(draftVal) !== JSON.stringify(initialVal);
247
-
248
- if (!isModified) {
249
- if (cleanIfEqual && JSON.stringify(draftVal) === JSON.stringify(transformedServerVal)) {
250
- _deletePathValue(entityInstance.draftData, path);
251
- } else {
252
- _setPathValue(entityInstance.draftData, path, transformedServerVal);
253
- }
254
- }
255
- });
256
- }
257
- }
258
-
259
- /**
260
- * Applique `syncDraftOnServerChange()` pour tous les champs autorisés de l'entité.
261
- * @param {object} entityInstance
262
- */
263
- export function autoSyncDraftFromSchema(entityInstance) {
264
- const constants = Array.isArray(entityInstance.constructor.SCHEMA_CONSTANTS)
265
- ? entityInstance.constructor.SCHEMA_CONSTANTS
266
- : [entityInstance.constructor.SCHEMA_CONSTANTS];
267
-
268
- const combinedSchema = { allOf: [], $defs: {} };
269
-
270
- for (const key of constants) {
271
- const sch = entityInstance.apiClient.getRequestSchema(key);
272
- if (!sch) throw new ApiError(`Unable to find schema for ${key}.`);
273
- if (sch.$defs) {
274
- for (const [defKey, defVal] of Object.entries(sch.$defs)) {
275
- if (!combinedSchema.$defs[defKey]) {
276
- combinedSchema.$defs[defKey] = defVal;
277
- }
278
- }
279
- }
280
- combinedSchema.allOf.push(sch);
281
- }
282
-
283
- const data = {
284
- ...entityInstance.serverData,
285
- ...entityInstance.defaultFields
286
- };
287
-
288
- let allowed = entityInstance._extractWritableFields?.(combinedSchema, data) || [];
289
- allowed = allowed.filter(k => !entityInstance.removeFields.includes(k));
290
- allowed = allowed.filter(k => !["id"].includes(k));
291
-
292
- syncDraftOnServerChange(entityInstance, allowed);
293
- }