@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/dist/cocolight-api-client.browser.js +2 -2
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js.map +1 -1
- package/package.json +1 -1
- package/src/api/BaseEntity.js +143 -49
- package/src/utils/reactive.js +6 -95
package/package.json
CHANGED
package/src/api/BaseEntity.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
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 (
|
|
245
|
-
this.
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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]
|
|
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
|
-
|
|
801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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
|
|
1021
|
+
const initial = structuredClone ? structuredClone(rawDraft) : JSON.parse(JSON.stringify(rawDraft));
|
|
920
1022
|
|
|
921
|
-
|
|
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
|
}
|
package/src/utils/reactive.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|