@communecter/cocolight-api-client 1.0.145 → 1.0.146
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 +1 -1
- 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/ApiClient.ts +66 -2
- package/src/api/Answer.ts +114 -1
- package/src/api/Form.ts +81 -1
- package/types/ApiClient.d.ts +20 -1
- package/types/api/Answer.d.ts +84 -0
- package/types/api/Form.d.ts +51 -0
package/package.json
CHANGED
package/src/ApiClient.ts
CHANGED
|
@@ -54,6 +54,17 @@ export interface ApiClientOptions {
|
|
|
54
54
|
circuitBreakerThreshold?: number;
|
|
55
55
|
circuitBreakerResetTime?: number;
|
|
56
56
|
fromJSONValue?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Mode de dialogue avec le backend cocolight-backend :
|
|
59
|
+
* - `legacy` (défaut) : réponses byte-compatibles PHP ; la lib applique TOUTES ses
|
|
60
|
+
* normalisations côté client (_transformData/_normalizeJsonData) + validation AJV.
|
|
61
|
+
* - `clean` : envoie `X-Coco-Mode: clean` ; le SERVEUR applique les normalisations et
|
|
62
|
+
* répond {success, data|error}. La lib unwrap l'enveloppe, lève ApiResponseError sur
|
|
63
|
+
* success:false, SAUTE _transformData et la validation AJV (schémas legacy), et revive
|
|
64
|
+
* (dates ISO -> Date, id -> _id ObjectID) pour rendre à l'app le MÊME shape qu'avant.
|
|
65
|
+
* NB : l'intercepteur refresh-token reste en legacy (appel interne sans le header).
|
|
66
|
+
*/
|
|
67
|
+
mode?: "legacy" | "clean";
|
|
57
68
|
tokenStorageStrategy?: TokenStorageStrategy | null;
|
|
58
69
|
}
|
|
59
70
|
|
|
@@ -136,6 +147,7 @@ export default class ApiClient extends EventEmitter {
|
|
|
136
147
|
|
|
137
148
|
// Internal userId setter
|
|
138
149
|
private _setUserId: (id: string | null) => void;
|
|
150
|
+
private _mode: "legacy" | "clean" = "legacy";
|
|
139
151
|
constructor({
|
|
140
152
|
baseURL,
|
|
141
153
|
accessToken,
|
|
@@ -148,6 +160,7 @@ export default class ApiClient extends EventEmitter {
|
|
|
148
160
|
circuitBreakerThreshold = 5,
|
|
149
161
|
circuitBreakerResetTime = 60000,
|
|
150
162
|
fromJSONValue = true,
|
|
163
|
+
mode = "legacy",
|
|
151
164
|
tokenStorageStrategy = null
|
|
152
165
|
}: ApiClientOptions) {
|
|
153
166
|
super(); // EventEmitter
|
|
@@ -161,6 +174,7 @@ export default class ApiClient extends EventEmitter {
|
|
|
161
174
|
this._endpoints = endpoints || endpointsJson.endpoints;
|
|
162
175
|
this._debug = debug;
|
|
163
176
|
this._fromJSONValue = fromJSONValue;
|
|
177
|
+
this._mode = mode === "clean" ? "clean" : "legacy";
|
|
164
178
|
this._breakerThreshold = circuitBreakerThreshold;
|
|
165
179
|
this._breakerResetTime = circuitBreakerResetTime;
|
|
166
180
|
|
|
@@ -697,6 +711,9 @@ export default class ApiClient extends EventEmitter {
|
|
|
697
711
|
const lowerMethod = (method || "GET").toLowerCase();
|
|
698
712
|
const realContentType = contentType || "application/json";
|
|
699
713
|
const headers: Record<string, string> = { "Content-Type": realContentType };
|
|
714
|
+
if (this._mode === "clean") {
|
|
715
|
+
headers["X-Coco-Mode"] = "clean";
|
|
716
|
+
}
|
|
700
717
|
|
|
701
718
|
// Auth headers
|
|
702
719
|
if (this._accessToken) {
|
|
@@ -844,7 +861,20 @@ export default class ApiClient extends EventEmitter {
|
|
|
844
861
|
[lowerMethod === "get" ? "params" : "data"]: payload
|
|
845
862
|
});
|
|
846
863
|
|
|
847
|
-
if (
|
|
864
|
+
if (this._mode === "clean") {
|
|
865
|
+
// Enveloppe serveur {success, data|error} : unwrap + erreur typée. Les normalisations
|
|
866
|
+
// sont faites PAR LE SERVEUR (serializeClean) — pas de _transformData ni d'AJV legacy ici.
|
|
867
|
+
const body = response.data;
|
|
868
|
+
if (body && typeof body === "object" && !Array.isArray(body) && typeof body.success === "boolean") {
|
|
869
|
+
if (body.success === false) {
|
|
870
|
+
throw new ApiResponseError(body?.error?.message ?? "Erreur inconnue", response.status, body);
|
|
871
|
+
}
|
|
872
|
+
response.data = body.data;
|
|
873
|
+
}
|
|
874
|
+
if (this._fromJSONValue) {
|
|
875
|
+
response.data = this._reviveClean(response.data);
|
|
876
|
+
}
|
|
877
|
+
} else if (validateResponseSchema) {
|
|
848
878
|
const statusStr = String(response.status);
|
|
849
879
|
const schema = responses?.[statusStr];
|
|
850
880
|
|
|
@@ -863,7 +893,7 @@ export default class ApiClient extends EventEmitter {
|
|
|
863
893
|
|
|
864
894
|
if (typeof transformResponseData === "function") {
|
|
865
895
|
response.data = transformResponseData(response.data);
|
|
866
|
-
} else if (transformResponseData === true) {
|
|
896
|
+
} else if (transformResponseData === true && this._mode !== "clean") {
|
|
867
897
|
response.data = this._transformData(response.data);
|
|
868
898
|
}
|
|
869
899
|
|
|
@@ -914,6 +944,12 @@ export default class ApiClient extends EventEmitter {
|
|
|
914
944
|
|
|
915
945
|
return response;
|
|
916
946
|
} catch (error) {
|
|
947
|
+
if (error instanceof ApiResponseError) {
|
|
948
|
+
// Erreur MÉTIER (enveloppe du mode clean success:false) : même sémantique que le
|
|
949
|
+
// checkAndThrowApiResponseError legacy -> re-throw tel quel, et SANS compter dans le
|
|
950
|
+
// circuit breaker (en legacy ces erreurs arrivent en HTTP 200 et ne l'incrémentent pas).
|
|
951
|
+
throw error;
|
|
952
|
+
}
|
|
917
953
|
this._updateCircuitBreakerError();
|
|
918
954
|
this._logger.error(`[ApiClient] Erreur lors de l'appel de ${constant}: ${getErrorMessage(error)}`);
|
|
919
955
|
if(error instanceof ApiValidationError) {
|
|
@@ -1234,6 +1270,34 @@ export default class ApiClient extends EventEmitter {
|
|
|
1234
1270
|
*
|
|
1235
1271
|
* @private
|
|
1236
1272
|
*/
|
|
1273
|
+
/**
|
|
1274
|
+
* Reviver du mode clean : redonne à l'app le shape exact qu'elle recevait après les
|
|
1275
|
+
* normalisations legacy de la lib — dates ISO (champs _dateFields) -> instances Date,
|
|
1276
|
+
* `id` oid-hex -> `_id` instance ObjectID (créée via EJSON.fromJSONValue, MÊME chemin
|
|
1277
|
+
* que le mode legacy). Le serveur clean a déjà fait tout le reste.
|
|
1278
|
+
*/
|
|
1279
|
+
private _reviveClean(data: any): any {
|
|
1280
|
+
if (Array.isArray(data)) {
|
|
1281
|
+
return data.map((item) => this._reviveClean(item));
|
|
1282
|
+
}
|
|
1283
|
+
if (data !== null && typeof data === "object") {
|
|
1284
|
+
const out: any = {};
|
|
1285
|
+
Object.keys(data).forEach((key) => {
|
|
1286
|
+
let value = this._reviveClean(data[key]);
|
|
1287
|
+
if (this._dateFields.includes(key) && typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) {
|
|
1288
|
+
const d = new Date(value);
|
|
1289
|
+
if (!Number.isNaN(d.getTime())) value = d;
|
|
1290
|
+
}
|
|
1291
|
+
out[key] = value;
|
|
1292
|
+
});
|
|
1293
|
+
if (typeof out.id === "string" && /^[0-9a-fA-F]{24}$/.test(out.id) && out._id === undefined) {
|
|
1294
|
+
out._id = EJSON.fromJSONValue({ $type: "oid", $value: out.id });
|
|
1295
|
+
}
|
|
1296
|
+
return out;
|
|
1297
|
+
}
|
|
1298
|
+
return data;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1237
1301
|
private _transformData(data: any): any {
|
|
1238
1302
|
if (data && typeof data === "object") {
|
|
1239
1303
|
if (data.resultGoods?.msg) {
|
package/src/api/Answer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseEntity } from "./BaseEntity.js";
|
|
2
2
|
import { ApiError } from "../error.js";
|
|
3
3
|
|
|
4
|
-
import type { CoformAnswersByIdData, CoformGetAnswerFilesData, CoformUploadAnswerFileData, DeleteElementData, SaveCoformAnswerData } from "./EndpointApi.types.js";
|
|
4
|
+
import type { CoformAnswersByIdData, CoformGetAnswerFilesData, CoformUploadAnswerFileData, DeleteElementData, GetCoformAnswerHistoryData, GetCoformMultievalDataData, SaveCoformAnswerData } from "./EndpointApi.types.js";
|
|
5
5
|
import type { AnswerItemNormalized } from "./serverDataType/Answer.js";
|
|
6
6
|
import type { FormItemNormalized } from "./serverDataType/Form.js";
|
|
7
7
|
|
|
@@ -13,6 +13,51 @@ type UploadInput = File | Blob | Buffer | import("stream").Readable;
|
|
|
13
13
|
*/
|
|
14
14
|
export type AllStepsData = Record<string, Record<string, unknown>>;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Une entrée de l'historique d'audit d'une réponse (réponse de
|
|
18
|
+
* `GET_COFORM_ANSWER_HISTORY`). `userName`/`userSlug` sont dénormalisés
|
|
19
|
+
* au moment de la modification : survivent à une suppression de l'user.
|
|
20
|
+
*/
|
|
21
|
+
export interface AnswerChange {
|
|
22
|
+
userId: string;
|
|
23
|
+
userName: string;
|
|
24
|
+
userSlug: string;
|
|
25
|
+
/** Timestamp unix en SECONDES (pas ms) — `new Date(at * 1000)`. */
|
|
26
|
+
at: number;
|
|
27
|
+
/** `""` possible : cast PHP `(string)` avec défaut vide si champ absent. */
|
|
28
|
+
mutationType: "create" | "update" | "";
|
|
29
|
+
changedFields: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Un axe du radar multi-eval (= un input radioNew d'une step donnée). */
|
|
33
|
+
export interface MultiEvalAxis {
|
|
34
|
+
key: string;
|
|
35
|
+
label: string;
|
|
36
|
+
options: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Un dataset = la contribution d'un user à un step (1 dataset = 1 user). */
|
|
40
|
+
export interface MultiEvalDataset {
|
|
41
|
+
userId: string;
|
|
42
|
+
userName: string;
|
|
43
|
+
userSlug: string;
|
|
44
|
+
evaluatedAt: number | null;
|
|
45
|
+
values: Record<string, number>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Une step avec ses axes et ses contributeurs (élément de `getMultiEvalData`). */
|
|
49
|
+
export interface MultiEvalStepData {
|
|
50
|
+
stepKey: string;
|
|
51
|
+
stepName: string;
|
|
52
|
+
axes: MultiEvalAxis[];
|
|
53
|
+
datasets: MultiEvalDataset[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Réponse de `Answer.getMultiEvalData()`. */
|
|
57
|
+
export interface MultiEvalDataResponse {
|
|
58
|
+
steps: MultiEvalStepData[];
|
|
59
|
+
}
|
|
60
|
+
|
|
16
61
|
/**
|
|
17
62
|
* Valeur d'un upload en attente — objet `{ name?, data: "data:...;base64,..." }`
|
|
18
63
|
* inséré par les inputs uploader avant le pré-upload backend.
|
|
@@ -517,6 +562,74 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
517
562
|
}));
|
|
518
563
|
}
|
|
519
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Récupère l'historique d'audit de cette Answer.
|
|
567
|
+
*
|
|
568
|
+
* Trace `create` + `update` triée du plus récent au plus ancien, max 200
|
|
569
|
+
* entrées (cap backend). Auth côté serveur : owner OU canAdminAnswer
|
|
570
|
+
* (admin du parent du form, admin/membre finder selon
|
|
571
|
+
* `membersCanEditSharedAnswer`, ou form `publicCanEditSharedAnswer`).
|
|
572
|
+
*
|
|
573
|
+
* Constant : `GET_COFORM_ANSWER_HISTORY` (POST
|
|
574
|
+
* `/survey/coform/getanswerhistory`, auth bearer).
|
|
575
|
+
*
|
|
576
|
+
* @returns Liste des changements ; tableau vide si l'historique est vide.
|
|
577
|
+
* @throws {ApiError} 400 si Answer sans id.
|
|
578
|
+
* @throws {ApiAuthenticationError} si non connecté.
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* const form = await org.form({ id: formId });
|
|
582
|
+
* const answer = await form.answer({ id: answerId });
|
|
583
|
+
* const history = await answer.getHistory();
|
|
584
|
+
* new Date(history[0].at * 1000); // date de la dernière modif (at en SECONDES)
|
|
585
|
+
*/
|
|
586
|
+
async getHistory(): Promise<AnswerChange[]> {
|
|
587
|
+
if (!this.id) {
|
|
588
|
+
throw new ApiError("Answer sans id, impossible de récupérer son historique.", 400);
|
|
589
|
+
}
|
|
590
|
+
const payload: GetCoformAnswerHistoryData = { answerId: this.id };
|
|
591
|
+
// EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {history}}.
|
|
592
|
+
const body = await this.callIsConnected(() =>
|
|
593
|
+
this.endpointApi.getCoformAnswerHistory(payload)
|
|
594
|
+
) as { result?: boolean; data?: { history?: AnswerChange[] } };
|
|
595
|
+
const list = body?.data?.history;
|
|
596
|
+
return Array.isArray(list) ? list : [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Récupère les datasets agrégés des évaluations multiples (radar) de cette
|
|
601
|
+
* Answer. Pour une réponse partagée contenant des inputs `radioNew` avec
|
|
602
|
+
* `activeMultieval: true`, retourne un dataset par contributeur pour
|
|
603
|
+
* chaque step.
|
|
604
|
+
*
|
|
605
|
+
* Constant : `GET_COFORM_MULTIEVAL_DATA` (POST
|
|
606
|
+
* `/survey/coform/getmultievaldata`, auth bearer). Auth côté serveur :
|
|
607
|
+
* owner OU canAdminAnswer.
|
|
608
|
+
*
|
|
609
|
+
* @param opts.stepKey - Optionnel : filtrer aux datasets d'une seule
|
|
610
|
+
* step. Sinon retourne toutes les steps.
|
|
611
|
+
* @throws {ApiError} 400 si Answer sans id.
|
|
612
|
+
* @throws {ApiAuthenticationError} si non connecté.
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* const data = await answer.getMultiEvalData();
|
|
616
|
+
* data.steps.forEach(s => renderRadar(s));
|
|
617
|
+
*/
|
|
618
|
+
async getMultiEvalData(opts: { stepKey?: string | null } = {}): Promise<MultiEvalDataResponse> {
|
|
619
|
+
if (!this.id) {
|
|
620
|
+
throw new ApiError("Answer sans id, impossible de récupérer ses évaluations multiples.", 400);
|
|
621
|
+
}
|
|
622
|
+
const payload: GetCoformMultievalDataData = { answerId: this.id };
|
|
623
|
+
if (opts.stepKey) payload.stepKey = opts.stepKey;
|
|
624
|
+
|
|
625
|
+
// EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {steps}}.
|
|
626
|
+
const body = await this.callIsConnected(() =>
|
|
627
|
+
this.endpointApi.getCoformMultievalData(payload)
|
|
628
|
+
) as { result?: boolean; data?: { steps?: MultiEvalStepData[] } };
|
|
629
|
+
const steps = body?.data?.steps;
|
|
630
|
+
return { steps: Array.isArray(steps) ? steps : [] };
|
|
631
|
+
}
|
|
632
|
+
|
|
520
633
|
/**
|
|
521
634
|
* Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
|
|
522
635
|
*
|
package/src/api/Form.ts
CHANGED
|
@@ -3,9 +3,40 @@ import { ApiError } from "../error.js";
|
|
|
3
3
|
|
|
4
4
|
import type { Answer } from "./Answer.js";
|
|
5
5
|
import type { PaginatorPage, PaginatorState } from "./BaseEntity.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
CoformAnswersSearchData,
|
|
8
|
+
GetCoformCatalogsData,
|
|
9
|
+
} from "./EndpointApi.types.js";
|
|
7
10
|
import type { CoformCommonTableContributor, FormItemNormalized } from "./serverDataType/Form.js";
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Une entrée de catalogue commonTable (une criteria agrégée cross-réponses).
|
|
14
|
+
* `label`/`usage`/`usageKey`/`coeff`/`count` toujours présents (le backend
|
|
15
|
+
* les construit avec défauts) ; `names`/`name` seulement si au moins une
|
|
16
|
+
* réponse `yesOrNo` non vide existe pour la criteria. Les champs echo
|
|
17
|
+
* (`label`, `usage`…) renvoient la valeur stockée brute, sans coercion.
|
|
18
|
+
*/
|
|
19
|
+
export interface CommonTableCatalogEntry {
|
|
20
|
+
label: unknown;
|
|
21
|
+
usage: unknown;
|
|
22
|
+
usageKey: unknown;
|
|
23
|
+
coeff: number | string | boolean | null;
|
|
24
|
+
count: number;
|
|
25
|
+
/** Distribution `{ solutionName → fréquence }` ; absent si aucune réponse. */
|
|
26
|
+
names?: Record<string, number>;
|
|
27
|
+
/** Mode (plus fréquent) de `names` ; présent ssi `names` présent. */
|
|
28
|
+
name?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Catalogue collaboratif des inputs `commonTable` : map
|
|
33
|
+
* `inputKey → (criteriaId → CommonTableCatalogEntry)`. Les entrées sont
|
|
34
|
+
* DIRECTEMENT sous l'inputKey (pas de niveau intermédiaire `entries`).
|
|
35
|
+
* Format aligné sur la réponse backend de `GET_COFORM_CATALOGS`
|
|
36
|
+
* (`getCatalogs` normalise les `[]` PHP — catalogues vides — en `{}`).
|
|
37
|
+
*/
|
|
38
|
+
export type CommonTableCatalogs = Record<string, Record<string, CommonTableCatalogEntry>>;
|
|
39
|
+
|
|
9
40
|
export class Form extends BaseEntity<FormItemNormalized> {
|
|
10
41
|
static override entityType = "forms";
|
|
11
42
|
|
|
@@ -199,4 +230,53 @@ export class Form extends BaseEntity<FormItemNormalized> {
|
|
|
199
230
|
|
|
200
231
|
return res?.data?.contributors ?? [];
|
|
201
232
|
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Récupère en UN appel batch les catalogues collaboratifs des inputs
|
|
236
|
+
* `commonTable` de CE formulaire. Pour chaque `inputKey`, retourne les
|
|
237
|
+
* `criterias` agrégées par toutes les réponses + le comptage par
|
|
238
|
+
* `criteriaId`.
|
|
239
|
+
*
|
|
240
|
+
* Constant : `GET_COFORM_CATALOGS` (POST `/survey/coform/getformcatalogs`,
|
|
241
|
+
* auth: none — accessible aux visiteurs anonymes pour pré-remplir les
|
|
242
|
+
* suggestions côté input).
|
|
243
|
+
*
|
|
244
|
+
* Si `inputKeys` est vide, on lève une erreur — le caller doit gate
|
|
245
|
+
* l'appel en amont (cf. `useCoFormCatalogs` qui passe `enabled: false`).
|
|
246
|
+
*
|
|
247
|
+
* @param params.inputKeys - `fieldKey` de tous les inputs commonTable.
|
|
248
|
+
* @returns Map `inputKey → (criteriaId → entry)` ; `{}` pour un inputKey sans données.
|
|
249
|
+
* @throws {ApiError} 400 si Form sans id ou `inputKeys` vide.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* const form = await api.form({ id: formId });
|
|
253
|
+
* const catalogs = await form.getCatalogs({ inputKeys: ["usagesEtSolutions"] });
|
|
254
|
+
* catalogs["usagesEtSolutions"]; // {criteriaId: {label, count, name?, names?, ...}}
|
|
255
|
+
*/
|
|
256
|
+
async getCatalogs(params: { inputKeys: string[] }): Promise<CommonTableCatalogs> {
|
|
257
|
+
if (!this.id) throw new ApiError("Form sans id, impossible de récupérer ses catalogues.", 400);
|
|
258
|
+
if (!Array.isArray(params?.inputKeys) || params.inputKeys.length === 0) {
|
|
259
|
+
throw new ApiError("`inputKeys` requis et non vide pour getCatalogs.", 400);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const requestData: GetCoformCatalogsData = {
|
|
263
|
+
formId: this.id,
|
|
264
|
+
// contentType form-urlencoded → sérialisation JSON string requise.
|
|
265
|
+
inputKeys: JSON.stringify(params.inputKeys),
|
|
266
|
+
};
|
|
267
|
+
// EndpointApi.call retourne le body HTTP ; succès serveur = {result: true, data: {inputKey: …}}.
|
|
268
|
+
// Quirk json_encode PHP : un catalogue vide (par clé, ou tout `data`) arrive en `[]` au lieu
|
|
269
|
+
// de `{}` — on normalise pour garantir le type map au caller.
|
|
270
|
+
const body = (await this.endpointApi.getCoformCatalogs(requestData)) as {
|
|
271
|
+
result?: boolean;
|
|
272
|
+
data?: Record<string, Record<string, CommonTableCatalogEntry> | []> | [];
|
|
273
|
+
};
|
|
274
|
+
const data = body?.data;
|
|
275
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return {};
|
|
276
|
+
const catalogs: CommonTableCatalogs = {};
|
|
277
|
+
for (const [inputKey, catalog] of Object.entries(data)) {
|
|
278
|
+
catalogs[inputKey] = catalog && typeof catalog === "object" && !Array.isArray(catalog) ? catalog : {};
|
|
279
|
+
}
|
|
280
|
+
return catalogs;
|
|
281
|
+
}
|
|
202
282
|
}
|
package/types/ApiClient.d.ts
CHANGED
|
@@ -24,6 +24,17 @@ export interface ApiClientOptions {
|
|
|
24
24
|
circuitBreakerThreshold?: number;
|
|
25
25
|
circuitBreakerResetTime?: number;
|
|
26
26
|
fromJSONValue?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Mode de dialogue avec le backend cocolight-backend :
|
|
29
|
+
* - `legacy` (défaut) : réponses byte-compatibles PHP ; la lib applique TOUTES ses
|
|
30
|
+
* normalisations côté client (_transformData/_normalizeJsonData) + validation AJV.
|
|
31
|
+
* - `clean` : envoie `X-Coco-Mode: clean` ; le SERVEUR applique les normalisations et
|
|
32
|
+
* répond {success, data|error}. La lib unwrap l'enveloppe, lève ApiResponseError sur
|
|
33
|
+
* success:false, SAUTE _transformData et la validation AJV (schémas legacy), et revive
|
|
34
|
+
* (dates ISO -> Date, id -> _id ObjectID) pour rendre à l'app le MÊME shape qu'avant.
|
|
35
|
+
* NB : l'intercepteur refresh-token reste en legacy (appel interne sans le header).
|
|
36
|
+
*/
|
|
37
|
+
mode?: "legacy" | "clean";
|
|
27
38
|
tokenStorageStrategy?: TokenStorageStrategy | null;
|
|
28
39
|
}
|
|
29
40
|
/**
|
|
@@ -81,7 +92,8 @@ export default class ApiClient extends EventEmitter {
|
|
|
81
92
|
private _accessToken;
|
|
82
93
|
private _refreshToken;
|
|
83
94
|
private _setUserId;
|
|
84
|
-
|
|
95
|
+
private _mode;
|
|
96
|
+
constructor({ baseURL, accessToken, refreshToken, refreshUrl, endpoints, timeout, debug, maxRetries, circuitBreakerThreshold, circuitBreakerResetTime, fromJSONValue, mode, tokenStorageStrategy }: ApiClientOptions);
|
|
85
97
|
/**
|
|
86
98
|
* Sets the access token for the API client and updates the authorization header.
|
|
87
99
|
*/
|
|
@@ -236,6 +248,13 @@ export default class ApiClient extends EventEmitter {
|
|
|
236
248
|
*
|
|
237
249
|
* @private
|
|
238
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* Reviver du mode clean : redonne à l'app le shape exact qu'elle recevait après les
|
|
253
|
+
* normalisations legacy de la lib — dates ISO (champs _dateFields) -> instances Date,
|
|
254
|
+
* `id` oid-hex -> `_id` instance ObjectID (créée via EJSON.fromJSONValue, MÊME chemin
|
|
255
|
+
* que le mode legacy). Le serveur clean a déjà fait tout le reste.
|
|
256
|
+
*/
|
|
257
|
+
private _reviveClean;
|
|
239
258
|
private _transformData;
|
|
240
259
|
/**
|
|
241
260
|
* Normalizes JSON data by transforming specific fields and ensuring URLs are complete.
|
package/types/api/Answer.d.ts
CHANGED
|
@@ -7,6 +7,46 @@ type UploadInput = File | Blob | Buffer | import("stream").Readable;
|
|
|
7
7
|
* Générique : la lib ne fait aucune supposition sur le type des valeurs.
|
|
8
8
|
*/
|
|
9
9
|
export type AllStepsData = Record<string, Record<string, unknown>>;
|
|
10
|
+
/**
|
|
11
|
+
* Une entrée de l'historique d'audit d'une réponse (réponse de
|
|
12
|
+
* `GET_COFORM_ANSWER_HISTORY`). `userName`/`userSlug` sont dénormalisés
|
|
13
|
+
* au moment de la modification : survivent à une suppression de l'user.
|
|
14
|
+
*/
|
|
15
|
+
export interface AnswerChange {
|
|
16
|
+
userId: string;
|
|
17
|
+
userName: string;
|
|
18
|
+
userSlug: string;
|
|
19
|
+
/** Timestamp unix en SECONDES (pas ms) — `new Date(at * 1000)`. */
|
|
20
|
+
at: number;
|
|
21
|
+
/** `""` possible : cast PHP `(string)` avec défaut vide si champ absent. */
|
|
22
|
+
mutationType: "create" | "update" | "";
|
|
23
|
+
changedFields: string[];
|
|
24
|
+
}
|
|
25
|
+
/** Un axe du radar multi-eval (= un input radioNew d'une step donnée). */
|
|
26
|
+
export interface MultiEvalAxis {
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
options: string[];
|
|
30
|
+
}
|
|
31
|
+
/** Un dataset = la contribution d'un user à un step (1 dataset = 1 user). */
|
|
32
|
+
export interface MultiEvalDataset {
|
|
33
|
+
userId: string;
|
|
34
|
+
userName: string;
|
|
35
|
+
userSlug: string;
|
|
36
|
+
evaluatedAt: number | null;
|
|
37
|
+
values: Record<string, number>;
|
|
38
|
+
}
|
|
39
|
+
/** Une step avec ses axes et ses contributeurs (élément de `getMultiEvalData`). */
|
|
40
|
+
export interface MultiEvalStepData {
|
|
41
|
+
stepKey: string;
|
|
42
|
+
stepName: string;
|
|
43
|
+
axes: MultiEvalAxis[];
|
|
44
|
+
datasets: MultiEvalDataset[];
|
|
45
|
+
}
|
|
46
|
+
/** Réponse de `Answer.getMultiEvalData()`. */
|
|
47
|
+
export interface MultiEvalDataResponse {
|
|
48
|
+
steps: MultiEvalStepData[];
|
|
49
|
+
}
|
|
10
50
|
/**
|
|
11
51
|
* Valeur d'un upload en attente — objet `{ name?, data: "data:...;base64,..." }`
|
|
12
52
|
* inséré par les inputs uploader avant le pré-upload backend.
|
|
@@ -286,6 +326,50 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
286
326
|
* files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
|
|
287
327
|
*/
|
|
288
328
|
getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]>;
|
|
329
|
+
/**
|
|
330
|
+
* Récupère l'historique d'audit de cette Answer.
|
|
331
|
+
*
|
|
332
|
+
* Trace `create` + `update` triée du plus récent au plus ancien, max 200
|
|
333
|
+
* entrées (cap backend). Auth côté serveur : owner OU canAdminAnswer
|
|
334
|
+
* (admin du parent du form, admin/membre finder selon
|
|
335
|
+
* `membersCanEditSharedAnswer`, ou form `publicCanEditSharedAnswer`).
|
|
336
|
+
*
|
|
337
|
+
* Constant : `GET_COFORM_ANSWER_HISTORY` (POST
|
|
338
|
+
* `/survey/coform/getanswerhistory`, auth bearer).
|
|
339
|
+
*
|
|
340
|
+
* @returns Liste des changements ; tableau vide si l'historique est vide.
|
|
341
|
+
* @throws {ApiError} 400 si Answer sans id.
|
|
342
|
+
* @throws {ApiAuthenticationError} si non connecté.
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* const form = await org.form({ id: formId });
|
|
346
|
+
* const answer = await form.answer({ id: answerId });
|
|
347
|
+
* const history = await answer.getHistory();
|
|
348
|
+
* new Date(history[0].at * 1000); // date de la dernière modif (at en SECONDES)
|
|
349
|
+
*/
|
|
350
|
+
getHistory(): Promise<AnswerChange[]>;
|
|
351
|
+
/**
|
|
352
|
+
* Récupère les datasets agrégés des évaluations multiples (radar) de cette
|
|
353
|
+
* Answer. Pour une réponse partagée contenant des inputs `radioNew` avec
|
|
354
|
+
* `activeMultieval: true`, retourne un dataset par contributeur pour
|
|
355
|
+
* chaque step.
|
|
356
|
+
*
|
|
357
|
+
* Constant : `GET_COFORM_MULTIEVAL_DATA` (POST
|
|
358
|
+
* `/survey/coform/getmultievaldata`, auth bearer). Auth côté serveur :
|
|
359
|
+
* owner OU canAdminAnswer.
|
|
360
|
+
*
|
|
361
|
+
* @param opts.stepKey - Optionnel : filtrer aux datasets d'une seule
|
|
362
|
+
* step. Sinon retourne toutes les steps.
|
|
363
|
+
* @throws {ApiError} 400 si Answer sans id.
|
|
364
|
+
* @throws {ApiAuthenticationError} si non connecté.
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* const data = await answer.getMultiEvalData();
|
|
368
|
+
* data.steps.forEach(s => renderRadar(s));
|
|
369
|
+
*/
|
|
370
|
+
getMultiEvalData(opts?: {
|
|
371
|
+
stepKey?: string | null;
|
|
372
|
+
}): Promise<MultiEvalDataResponse>;
|
|
289
373
|
/**
|
|
290
374
|
* Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
|
|
291
375
|
*
|
package/types/api/Form.d.ts
CHANGED
|
@@ -3,6 +3,32 @@ import type { Answer } from "./Answer.js";
|
|
|
3
3
|
import type { PaginatorPage, PaginatorState } from "./BaseEntity.js";
|
|
4
4
|
import type { CoformAnswersSearchData } from "./EndpointApi.types.js";
|
|
5
5
|
import type { CoformCommonTableContributor, FormItemNormalized } from "./serverDataType/Form.js";
|
|
6
|
+
/**
|
|
7
|
+
* Une entrée de catalogue commonTable (une criteria agrégée cross-réponses).
|
|
8
|
+
* `label`/`usage`/`usageKey`/`coeff`/`count` toujours présents (le backend
|
|
9
|
+
* les construit avec défauts) ; `names`/`name` seulement si au moins une
|
|
10
|
+
* réponse `yesOrNo` non vide existe pour la criteria. Les champs echo
|
|
11
|
+
* (`label`, `usage`…) renvoient la valeur stockée brute, sans coercion.
|
|
12
|
+
*/
|
|
13
|
+
export interface CommonTableCatalogEntry {
|
|
14
|
+
label: unknown;
|
|
15
|
+
usage: unknown;
|
|
16
|
+
usageKey: unknown;
|
|
17
|
+
coeff: number | string | boolean | null;
|
|
18
|
+
count: number;
|
|
19
|
+
/** Distribution `{ solutionName → fréquence }` ; absent si aucune réponse. */
|
|
20
|
+
names?: Record<string, number>;
|
|
21
|
+
/** Mode (plus fréquent) de `names` ; présent ssi `names` présent. */
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Catalogue collaboratif des inputs `commonTable` : map
|
|
26
|
+
* `inputKey → (criteriaId → CommonTableCatalogEntry)`. Les entrées sont
|
|
27
|
+
* DIRECTEMENT sous l'inputKey (pas de niveau intermédiaire `entries`).
|
|
28
|
+
* Format aligné sur la réponse backend de `GET_COFORM_CATALOGS`
|
|
29
|
+
* (`getCatalogs` normalise les `[]` PHP — catalogues vides — en `{}`).
|
|
30
|
+
*/
|
|
31
|
+
export type CommonTableCatalogs = Record<string, Record<string, CommonTableCatalogEntry>>;
|
|
6
32
|
export declare class Form extends BaseEntity<FormItemNormalized> {
|
|
7
33
|
static entityType: string;
|
|
8
34
|
static entityTag: string;
|
|
@@ -105,4 +131,29 @@ export declare class Form extends BaseEntity<FormItemNormalized> {
|
|
|
105
131
|
inputKey: string;
|
|
106
132
|
criteriaIds: string | string[];
|
|
107
133
|
}): Promise<CoformCommonTableContributor[]>;
|
|
134
|
+
/**
|
|
135
|
+
* Récupère en UN appel batch les catalogues collaboratifs des inputs
|
|
136
|
+
* `commonTable` de CE formulaire. Pour chaque `inputKey`, retourne les
|
|
137
|
+
* `criterias` agrégées par toutes les réponses + le comptage par
|
|
138
|
+
* `criteriaId`.
|
|
139
|
+
*
|
|
140
|
+
* Constant : `GET_COFORM_CATALOGS` (POST `/survey/coform/getformcatalogs`,
|
|
141
|
+
* auth: none — accessible aux visiteurs anonymes pour pré-remplir les
|
|
142
|
+
* suggestions côté input).
|
|
143
|
+
*
|
|
144
|
+
* Si `inputKeys` est vide, on lève une erreur — le caller doit gate
|
|
145
|
+
* l'appel en amont (cf. `useCoFormCatalogs` qui passe `enabled: false`).
|
|
146
|
+
*
|
|
147
|
+
* @param params.inputKeys - `fieldKey` de tous les inputs commonTable.
|
|
148
|
+
* @returns Map `inputKey → (criteriaId → entry)` ; `{}` pour un inputKey sans données.
|
|
149
|
+
* @throws {ApiError} 400 si Form sans id ou `inputKeys` vide.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* const form = await api.form({ id: formId });
|
|
153
|
+
* const catalogs = await form.getCatalogs({ inputKeys: ["usagesEtSolutions"] });
|
|
154
|
+
* catalogs["usagesEtSolutions"]; // {criteriaId: {label, count, name?, names?, ...}}
|
|
155
|
+
*/
|
|
156
|
+
getCatalogs(params: {
|
|
157
|
+
inputKeys: string[];
|
|
158
|
+
}): Promise<CommonTableCatalogs>;
|
|
108
159
|
}
|