@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@communecter/cocolight-api-client",
3
- "version": "1.0.145",
3
+ "version": "1.0.146",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
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 (validateResponseSchema) {
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 { CoformAnswersSearchData } from "./EndpointApi.types.js";
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
  }
@@ -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
- constructor({ baseURL, accessToken, refreshToken, refreshUrl, endpoints, timeout, debug, maxRetries, circuitBreakerThreshold, circuitBreakerResetTime, fromJSONValue, tokenStorageStrategy }: ApiClientOptions);
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.
@@ -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
  *
@@ -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
  }