@communecter/cocolight-api-client 1.0.145 → 1.0.147

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.147",
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
  *
@@ -2,7 +2,7 @@
2
2
  import { ApiAuthenticationError } from "../error.js";
3
3
 
4
4
  import type ApiClient from "../ApiClient.js";
5
- import type { PersonRegisterData, AuthenticateUrlData, RefreshTokenUrlData, PasswordRecoveryData, ServerExchangeTokenData, ChangePasswordData, DeleteAccountData, UpdateSettingsData, UpdateBlockDescriptionData, UpdateBlockInfoData, UpdateBlockSocialData, UpdateBlockLocalityData, UpdateBlockSlugData, CheckData, ProfilImageData, ProfilBannerData, GetElementsAboutData, MulticonnectData, GetNewsData, GetNewsByIdData, AddNewsData, AddImageNewsData, AddFileNewsData, DeleteNewsData, UpdateNewsData, ShareNewsData, GetCommentsData, AddCommentsData, DeleteCommentsData, UpdateCommentsData, SearchTagsData, ShowVoteData, GlobalAutocompleteData, CityAutocompleteData, CityAutocompleteByCountryData, SuggestionInputData, GetProjectsNoAdminData, GetProjectsAdminData, GetPoisNoAdminData, GetPoisAdminData, GetOrganizationsNoAdminData, GetOrganizationsAdminData, GetUserEligiblePlacesData, GetMembersNoAdminData, GetMembersAdminData, GetFriendsAdminData, GetSubscriptionsData, GetSubscriptionsAdminData, GetSubscribersData, GetSubscribersAdminData, GetContributorsNoAdminData, GetContributorsAdminData, GetBadgesData, GetBadgesFiltersData, ConnectData, DisconnectData, GetElementsKeyData, GetFavorisData, DeleteFavorisData, AddFavorisData, AddOrganizationData, AddProjectData, AddPoiData, AddEventData, DeletePoiData, DeleteEventData, DeleteElementData, AddImageElementData, LinkValidateData, SearchMemberAutocompleteData, GetNotificationsData, GetNotificationsCountData, NotificationUpdateData, MarkNotificationAsReadData, ActivitypubSearchData, ActivitypubLinkData, ActivitypubGetCommunityData, GetBadgeData, AddBadgesData, AssignBadgesData, GetEventsData, ShareEventsData, InviteEventData, FollowData, GetCostumJsonData, GlobalAutocompleteCostumData, NavigatorGettlData, CostumEventRequestActorsData, CostumEventRequestSubeventsData, CostumEventRequestElementEventData, CostumEventRequestCategoriesData, CostumEventRequestDatesData, CostumEventRequestEventData, CostumEventRequestLinkTlToEventData, CostumEventRequestLoadContextTagData, GetGalleryData, GetAttendeesNoAdminData, GetAttendeesAdminData, CoformAnswersSearchData, CoformAnswersByIdData, GetCoformByIdData, CoformUploadAnswerFileData, CoformGetAnswerFilesData, GetCoformCatalogsData, SaveCoformAnswerData, GetCoformAnswerHistoryData, GetCoformMultievalDataData, GetCoformCommontableContributorsData, AddVoteData, AddReportAbuseData, UpdatePathValueData, DeleteDocumentByContextData, DeleteDocumentByIdData, DemoteAdminData, CostumFilterCoformData, CostumFilterCoformByPathData, GetCountriesData, SearchZonesData, CoformAnswersByFormsData, GenerateAnswerFromFormData, FundingEnvelopeData, CoremuOperationData, CostumProjectActionRequestNewData, CostumProjectActionRequestSetStatusData, CostumProjectActionRequestSetDateData, CostumProjectActionRequestSetContributorsData, CostumProjectActionRequestCancelData, CostumProjectActionRequestArchiveData, LinkDiscourseAccountData, UnlinkDiscourseAccountData, DiscourseProfileData, DiscourseCheckEmailData, DiscourseDismissLinkData, LinkMediawikiAccountData, UnlinkMediawikiAccountData, GetMediawikiContributionsData, AddClassifiedData } from "./EndpointApi.types.js";
5
+ import type { PersonRegisterData, AuthenticateUrlData, RefreshTokenUrlData, PasswordRecoveryData, ServerExchangeTokenData, ChangePasswordData, DeleteAccountData, UpdateSettingsData, UpdateBlockDescriptionData, UpdateBlockInfoData, UpdateBlockSocialData, UpdateBlockLocalityData, UpdateBlockSlugData, CheckData, ProfilImageData, ProfilBannerData, GetElementsAboutData, MulticonnectData, GetNewsData, GetNewsByIdData, AddNewsData, AddImageNewsData, AddFileNewsData, DeleteNewsData, UpdateNewsData, ShareNewsData, GetCommentsData, AddCommentsData, DeleteCommentsData, UpdateCommentsData, SearchTagsData, ShowVoteData, GlobalAutocompleteData, CityAutocompleteData, CityAutocompleteByCountryData, SuggestionInputData, GetProjectsNoAdminData, GetProjectsAdminData, GetPoisNoAdminData, GetPoisAdminData, GetOrganizationsNoAdminData, GetOrganizationsAdminData, GetUserEligiblePlacesData, GetMembersNoAdminData, GetMembersAdminData, GetFriendsAdminData, GetSubscriptionsData, GetSubscriptionsAdminData, GetSubscribersData, GetSubscribersAdminData, GetContributorsNoAdminData, GetContributorsAdminData, GetBadgesData, GetBadgesFiltersData, ConnectData, DisconnectData, GetElementsKeyData, GetFavorisData, DeleteFavorisData, AddFavorisData, AddOrganizationData, AddProjectData, AddPoiData, AddEventData, DeletePoiData, DeleteEventData, DeleteElementData, AddImageElementData, LinkValidateData, SearchMemberAutocompleteData, GetNotificationsData, GetNotificationsCountData, NotificationUpdateData, MarkNotificationAsReadData, ActivitypubSearchData, ActivitypubLinkData, ActivitypubGetCommunityData, GetBadgeData, AddBadgesData, AssignBadgesData, GetEventsData, ShareEventsData, InviteEventData, FollowData, GetCostumJsonData, GlobalAutocompleteCostumData, NavigatorGettlData, CostumEventRequestActorsData, CostumEventRequestSubeventsData, CostumEventRequestElementEventData, CostumEventRequestCategoriesData, CostumEventRequestDatesData, CostumEventRequestEventData, CostumEventRequestLinkTlToEventData, CostumEventRequestLoadContextTagData, GetGalleryData, GetAttendeesNoAdminData, GetAttendeesAdminData, CoformAnswersSearchData, CoformAnswersByIdData, GetCoformByIdData, CoformUploadAnswerFileData, CoformGetAnswerFilesData, GetCoformCatalogsData, SaveCoformAnswerData, GetCoformAnswerHistoryData, GetCoformMultievalDataData, GetCoformCommontableContributorsData, AddVoteData, AddReportAbuseData, UpdatePathValueData, DeleteDocumentByContextData, DeleteDocumentByIdData, DemoteAdminData, CostumFilterCoformData, CostumFilterCoformByPathData, GetCountriesData, SearchZonesData, CoformAnswersByFormsData, GenerateAnswerFromFormData, FundingEnvelopeData, CoremuOperationData, CostumProjectActionRequestNewData, CostumProjectActionRequestSetStatusData, CostumProjectActionRequestSetDateData, CostumProjectActionRequestSetContributorsData, CostumProjectActionRequestCancelData, CostumProjectActionRequestArchiveData, LinkDiscourseAccountData, UnlinkDiscourseAccountData, DiscourseProfileData, DiscourseCheckEmailData, DiscourseDismissLinkData, LinkMediawikiAccountData, UnlinkMediawikiAccountData, GetMediawikiContributionsData, AddClassifiedData, PersonActivateData } from "./EndpointApi.types.js";
6
6
 
7
7
  /**
8
8
  * Classe EndpointApi générée automatiquement depuis endpoints-copie.json
@@ -2251,6 +2251,21 @@ export class EndpointApi {
2251
2251
  return this.callIsConnected("ADD_CLASSIFIED", data);
2252
2252
  }
2253
2253
 
2254
+ /**
2255
+ * Activation d’un compte (lien email) : Valide l’adresse email d’un compte via le lien reçu par email. validationKey = sha256(user + email). Retire roles.tobeactivated et pose le consentement preferences.sendMail.source (port ActivateAction + Person::validateEmailAccount/validateUser).
2256
+ * Constant : PERSON_ACTIVATE
2257
+ * @param data - Données envoyées à l'API
2258
+ * @returns Les données de réponse.
2259
+ * @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
2260
+ * @throws {Error} - En cas d'erreur inattendue.
2261
+ */
2262
+ async personActivate(data: PersonActivateData): Promise<any> {
2263
+ if (!data || typeof data !== "object") {
2264
+ throw new TypeError("Le paramètre data doit être un objet.");
2265
+ }
2266
+ return this.callNoConnected("PERSON_ACTIVATE", data);
2267
+ }
2268
+
2254
2269
  }
2255
2270
 
2256
2271
  export default EndpointApi;
@@ -6663,3 +6663,16 @@ export interface AddClassifiedData {
6663
6663
  scope: "";
6664
6664
  [k: string]: unknown;
6665
6665
  }
6666
+
6667
+
6668
+ export interface PersonActivateData {
6669
+ /**
6670
+ * Identifiant (_id) du compte à activer
6671
+ */
6672
+ user: string;
6673
+ /**
6674
+ * Clé de validation = sha256(user + email)
6675
+ */
6676
+ validationKey: string;
6677
+ [k: string]: unknown;
6678
+ }
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
  }
@@ -148,4 +148,34 @@ export class UserApi {
148
148
  return response.data;
149
149
  });
150
150
  }
151
+
152
+ /**
153
+ * Active (valide l'email) un compte via le lien reçu par email.
154
+ *
155
+ * Endpoint public (aucune authentification requise). La `validationKey`
156
+ * correspond à `sha256(user + email)`. En cas de succès, le rôle
157
+ * `tobeactivated` est retiré et le consentement `sendMail.source` posé.
158
+ *
159
+ * @param params
160
+ * @param params.user - Identifiant (`_id`) du compte à activer.
161
+ * @param params.validationKey - Clé de validation = `sha256(user + email)`.
162
+ * @returns Réponse brute de l'API (`{ result: true, msg: string }`).
163
+ * @throws {ApiError} En cas de mise en file (offline/circuit breaker).
164
+ * @throws {ApiResponseError} Si l'API retourne un échec (`result === false`).
165
+ */
166
+ async activateAccount({ user, validationKey }: { user: string; validationKey: string }): Promise<any> {
167
+ return this.client.safeCall(async () => {
168
+ const response = await this.client.callEndpoint("PERSON_ACTIVATE", { user, validationKey });
169
+
170
+ if (!isAxiosResponse(response)) {
171
+ throw new ApiError("Requête mise en file (offline/circuit breaker)", 503, response);
172
+ }
173
+
174
+ if (response?.data?.result === false) {
175
+ throw new ApiResponseError(response.data.msg, response.status, response.data);
176
+ }
177
+
178
+ return response.data;
179
+ });
180
+ }
151
181
  }