@communecter/cocolight-api-client 1.0.133 → 1.0.135

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.
@@ -36,6 +36,25 @@ export interface ProcessUploadsOptions {
36
36
  /** Nombre d'uploads parallèles par batch. Défaut : 4. */
37
37
  batchSize?: number;
38
38
  }
39
+ /**
40
+ * Options de `Answer.get()` — params optionnels du endpoint `COFORM_ANSWERS_BY_ID`.
41
+ */
42
+ export interface AnswerGetOptions {
43
+ /**
44
+ * Liste de champs à retourner côté backend (filtrage). Si absent, tous les
45
+ * champs sont renvoyés. Utile pour optimiser la taille de la réponse.
46
+ */
47
+ fields?: string[];
48
+ /**
49
+ * ID du formulaire parent. Utilisé côté backend pour calculer `canEdit` et
50
+ * `editDeniedReason` (droits d'édition contextuels). Si absent, **auto-injecté
51
+ * via `this.parent.id`** quand le parent est un `Form` (cas typique
52
+ * `form.answer({id})`).
53
+ */
54
+ formId?: string;
55
+ /** Chemin Finder (rarement utilisé). */
56
+ finderPath?: string;
57
+ }
39
58
  /**
40
59
  * Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
41
60
  * défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
@@ -72,6 +91,38 @@ export interface UploadAnswerFileResult {
72
91
  /** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
73
92
  answerId: string;
74
93
  }
94
+ /**
95
+ * Options de `Answer.getFiles()`.
96
+ */
97
+ export interface GetFilesOptions {
98
+ /** Clé d'identification de l'input (ex: `"subFormId.inputId"`). Requise. */
99
+ subKey: string;
100
+ /** Type de document attendu. Défaut backend : `"file"`. */
101
+ docType?: "image" | "file";
102
+ /** Clé de contenu utilisée lors de l'upload. Défaut backend : `"presentation"`. */
103
+ contentKey?: string;
104
+ }
105
+ /**
106
+ * Élément renvoyé par `Answer.getFiles()` — un document attaché à l'Answer.
107
+ * Les champs `imagePath`/`imageThumbPath`/`imageMediumPath` ne sont présents
108
+ * que pour `docType: "image"`.
109
+ */
110
+ export interface AnswerFileItem {
111
+ /** ID MongoDB du document (utilisable pour suppression via `deleteDocumentById`). */
112
+ docId: string;
113
+ /** Chemin relatif du document côté backend. */
114
+ docPath: string;
115
+ /** Nom original du fichier. */
116
+ name?: string;
117
+ /** Taille en octets. */
118
+ size?: number;
119
+ /** Chemin de l'image (uniquement si `docType: "image"`). */
120
+ imagePath?: string | null;
121
+ /** Chemin du thumbnail (uniquement si `docType: "image"`). */
122
+ imageThumbPath?: string | null;
123
+ /** Chemin de la version medium (uniquement si `docType: "image"`). */
124
+ imageMediumPath?: string | null;
125
+ }
75
126
  export declare class Answer extends BaseEntity<AnswerItemNormalized> {
76
127
  static entityType: string;
77
128
  static entityTag: string;
@@ -85,6 +136,34 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
85
136
  * Les autres sont calculés/posés par le backend.
86
137
  */
87
138
  removeFields: string[];
139
+ /**
140
+ * {@inheritDoc BaseEntity#isAuthor}
141
+ *
142
+ * Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
143
+ * comme la version par défaut de BaseEntity). Le `user` peut être :
144
+ * - un `string` (ID MongoDB brut renvoyé par le backend)
145
+ * - une instance `User` (après `_transformServerData` qui le link en entité)
146
+ *
147
+ * Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
148
+ * la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
149
+ */
150
+ /**
151
+ * {@inheritDoc BaseEntity#_isAdminViaHierarchy}
152
+ *
153
+ * Override Answer : la hiérarchie d'Answer passe par son **parent instance
154
+ * Form** (pas par `serverData.parent` direct comme pour `events`/`projects`/`poi`).
155
+ *
156
+ * Chaîne : `Answer → parent (Form) → form.serverData.parent ({[orgId]: {type, name}}) → check userContext.links`.
157
+ *
158
+ * Si le parent n'est pas un Form (cas `api.answer({id})` direct sans contexte),
159
+ * renvoie `false` — impossible de vérifier la hiérarchie sans le Form.
160
+ *
161
+ * @protected
162
+ */
163
+ protected _isAdminViaHierarchy(): boolean;
164
+ isAuthor(options?: {
165
+ silent?: boolean;
166
+ }): boolean;
88
167
  /**
89
168
  * Transforme les champs imbriqués (user, context etc.) en instances d'entités.
90
169
  * @param data - Les données brutes du serveur.
@@ -93,10 +172,36 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
93
172
  */
94
173
  protected _transformServerData(data: AnswerItemNormalized): AnswerItemNormalized;
95
174
  /**
96
- * Rafraîchit les données de l'entité depuis l'API.
97
- * Constant : COFORM_ANSWERS_BY_ID
98
- */
99
- get(): Promise<Record<string, any>>;
175
+ * Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
176
+ *
177
+ * **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
178
+ * `form.answer({id})`), le param `formId` est automatiquement envoyé pour
179
+ * que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
180
+ * Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
181
+ *
182
+ * @param opts - Params optionnels :
183
+ * - `fields` : filtrage des champs côté backend
184
+ * - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
185
+ * - `finderPath` : chemin Finder (rare)
186
+ *
187
+ * @example
188
+ * // Cas commun : form.answer({id}) → get() auto avec formId
189
+ * const answer = await form.answer({ id: answerId });
190
+ * answer.serverData.canEdit; // true/false calculé backend
191
+ * answer.serverData.editDeniedReason; // raison si false
192
+ *
193
+ * @example
194
+ * // Cas avancé : filtrage de champs pour optim perf
195
+ * const answer = await form.answer();
196
+ * answer._id(answerId);
197
+ * await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
198
+ */
199
+ get(opts?: AnswerGetOptions): Promise<Record<string, any>>;
200
+ /**
201
+ * Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
202
+ * @private
203
+ */
204
+ private _autoFormIdFromParent;
100
205
  form(): Promise<never>;
101
206
  /**
102
207
  * Supprime cette Answer côté serveur via `DELETE_ELEMENT`
@@ -106,18 +211,91 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
106
211
  * - `serverData` et `draftData` sont vidés (sans casser la réactivité)
107
212
  * - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
108
213
  *
214
+ * **Guard** : autorisation côté client si l'utilisateur courant est :
215
+ * - l'**auteur** de l'Answer (`serverData.user === userId`), OU
216
+ * - **admin** d'un parent du Form (org/project) via la hiérarchie
217
+ * (`form.serverData.parent[orgId]` × `userContext.links.memberOf[orgId].isAdmin`).
218
+ *
219
+ * Le check utilise `isAuthorOrAdmin({checkHierarchy: true})` qui s'appuie sur
220
+ * l'override `Answer._isAdminViaHierarchy` (remonte via parent instance Form).
221
+ *
222
+ * **Pré-requis** :
223
+ * - `this.parent` doit être un Form (cas `form.answer({id})`)
224
+ * - `userContext` (User connecté) doit avoir `serverData.links` chargés (cas `api.me()`)
225
+ * Sans ces pré-requis, seul `isAuthor` peut autoriser.
226
+ *
109
227
  * @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
110
228
  * @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
229
+ * @throws {ApiError} 403 si ni auteur ni admin du parent.
111
230
  *
112
231
  * @example
113
232
  * const answer = await form.answer({ id: "..." });
114
- * await answer.delete();
233
+ * await answer.delete(); // OK si auteur OU admin org/project parent du Form
115
234
  * answer._isDeleted; // true
116
235
  */
117
236
  delete(reason?: string): Promise<void>;
237
+ /**
238
+ * {@inheritDoc BaseEntity#deleteFile}
239
+ *
240
+ * Override avec **cleanup local** : après suppression backend, retire le
241
+ * `docId` des structures uploader normalisées dans `serverData.answers` et
242
+ * `_draftData.answers` (format `{updateDate, files: {docId: docPath}}`
243
+ * produit par `processUploads()` et `_transformServerData`).
244
+ *
245
+ * Le caller n'a pas besoin de refetch — `answer.serverData.answers[*][*].files`
246
+ * reflète directement l'état post-suppression.
247
+ *
248
+ * @example
249
+ * const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
250
+ * await answer.deleteFile(files[0].docId);
251
+ * // answer.serverData.answers[sf][input].files[files[0].docId] === undefined
252
+ */
253
+ deleteFile(docId: string): Promise<void>;
254
+ /**
255
+ * Walk `answers[subFormId][inputId]` et retire `docId` des structures
256
+ * `{updateDate, files: {docId: docPath}}` (format canonique uploader).
257
+ * Mutation in-place pour préserver la réactivité.
258
+ * @private
259
+ */
260
+ private _removeDocIdFromUploaderFields;
261
+ /**
262
+ * Récupère la liste des fichiers attachés à un input précis de cette Answer
263
+ * via `COFORM_GET_ANSWER_FILES`.
264
+ *
265
+ * **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
266
+ * en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
267
+ * recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
268
+ *
269
+ * Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
270
+ * les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
271
+ * — pas besoin d'appel réseau.
272
+ *
273
+ * @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
274
+ * @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
275
+ * Tableau vide si aucun fichier trouvé.
276
+ * @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
277
+ *
278
+ * @example
279
+ * const form = await org.form({ id: formId });
280
+ * const answer = await form.answer({ id: answerId });
281
+ * const files = await answer.getFiles({
282
+ * subKey: `${subFormId}.${inputId}`,
283
+ * docType: "image",
284
+ * });
285
+ * files[0].docId; // ID du document (pour suppression future)
286
+ * files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
287
+ */
288
+ getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]>;
118
289
  /**
119
290
  * Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
120
291
  *
292
+ * **Building block du pipeline** : conçu pour être utilisé via
293
+ * `processUploads()` suivi de `save()`. L'Answer créée par le premier upload
294
+ * est en état "placeholder" côté backend (champ `user` non posé) — `save()`
295
+ * finalise. Appeler `get()` ou `delete()` (via l'entité) sur une Answer
296
+ * upload-only sans save échouera : le backend la considère comme orpheline
297
+ * (`editDeniedReason: "not_owner"`, stub sans `data.id`).
298
+ *
121
299
  * **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
122
300
  * renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
123
301
  * — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
@@ -276,6 +454,18 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
276
454
  private _getUploadKeys;
277
455
  private _getValueAtPath;
278
456
  private _setValueAtPath;
457
+ /**
458
+ * Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
459
+ * inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
460
+ *
461
+ * Utilisé par `_transformServerData()` pour harmoniser les Answer en format
462
+ * legacy Array `[{docId, docPath}]` avec le format moderne (ce que
463
+ * `processUploads()` écrit).
464
+ *
465
+ * Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
466
+ * @private
467
+ */
468
+ private _normalizeLegacyUploaderInAnswers;
279
469
  /**
280
470
  * Normalise une valeur d'input uploader vers le format legacy backend :
281
471
  * `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
@@ -562,6 +562,32 @@ export declare class BaseEntity<TServerData = any> {
562
562
  qqfilename: string;
563
563
  qqtotalfilesize: number;
564
564
  }>;
565
+ /**
566
+ * Supprime un document (fichier ou image) par son `docId` MongoDB via
567
+ * `DELETE_DOCUMENT_BY_ID`.
568
+ *
569
+ * Méthode générique exposée sur toutes les entités — le endpoint backend
570
+ * opère par docId seul, sans contexte parent requis. Disponible ici pour
571
+ * être découvrable et **override-able** par les entités qui veulent un
572
+ * cleanup local après suppression (ex: `News.deleteFile` pourrait retirer
573
+ * l'id de `mediaImg.images` côté serverData).
574
+ *
575
+ * Ne met PAS à jour `serverData` après suppression — le caller doit
576
+ * invalider son cache local s'il en a un (cas typique : React Query).
577
+ *
578
+ * @param docId - ID MongoDB du document (24 hex)
579
+ * @throws {ApiError} 400 si `docId` invalide.
580
+ *
581
+ * @example
582
+ * // Depuis une Answer (suppression d'un fichier uploader)
583
+ * const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
584
+ * await answer.deleteFile(files[0].docId);
585
+ *
586
+ * @example
587
+ * // Depuis n'importe quelle entité qui possède un docId
588
+ * await news.deleteFile(imageDocId);
589
+ */
590
+ deleteFile(docId: string): Promise<void>;
565
591
  /**
566
592
  * Valide les entrées d'upload de fichiers.
567
593
  *
@@ -938,15 +964,24 @@ export declare class BaseEntity<TServerData = any> {
938
964
  */
939
965
  private _extractUserContext;
940
966
  /**
941
- * Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste)
942
- * @private
967
+ * Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste).
968
+ *
969
+ * Note : passé `protected` pour permettre l'override par les entités dont la
970
+ * hiérarchie n'est pas un simple champ `serverData.parent` ou `.organizer`
971
+ * (cf. `Answer._isAdminViaHierarchy` qui remonte via son parent instance Form).
972
+ *
973
+ * @protected
943
974
  */
944
- private _isAdminViaHierarchy;
975
+ protected _isAdminViaHierarchy(): boolean;
945
976
  /**
946
- * Vérifie si l'utilisateur est admin d'un parent spécifique
947
- * @private
977
+ * Vérifie si l'utilisateur est admin d'un parent spécifique.
978
+ *
979
+ * Note : passé `protected` pour être utilisable par les overrides de
980
+ * `_isAdminViaHierarchy()` (cf. `Answer._isAdminViaHierarchy`).
981
+ *
982
+ * @protected
948
983
  */
949
- private _isAdminOfParent;
984
+ protected _isAdminOfParent(parentId: string, parentType: string): boolean;
950
985
  /**
951
986
  * Construit dynamiquement des filtres sur un lien entre entités
952
987
  *
@@ -1503,11 +1538,15 @@ export declare class BaseEntity<TServerData = any> {
1503
1538
  *
1504
1539
  * @param options - Options de vérification.
1505
1540
  * @param options.silent - Si `true`, retourne `false` au lieu de lever une exception. Par défaut `true`.
1541
+ * @param options.checkHierarchy - Si `true`, propagé à `isAdmin` pour vérifier
1542
+ * également la hiérarchie parent (ex: `Answer.isAdmin({checkHierarchy: true})`
1543
+ * remonte vers org/project du Form parent).
1506
1544
  * @returns - `true` si l'utilisateur est l'auteur ou administrateur, `false` sinon.
1507
1545
  * @throws {ApiError} - Si `silent` est `false` et que les préconditions ne sont pas remplies.
1508
1546
  */
1509
1547
  isAuthorOrAdmin(options?: {
1510
1548
  silent?: boolean;
1549
+ checkHierarchy?: boolean;
1511
1550
  }): boolean;
1512
1551
  /**
1513
1552
  * Vérifie si l'utilisateur est membre de l'entité.
@@ -55,6 +55,12 @@ export interface AnswerItemJson {
55
55
  project?: AnswerProjectRef;
56
56
  [key: string]: unknown;
57
57
  }
58
+ /**
59
+ * Raisons de refus d'édition côté backend, calculées par `coformAnswersById`
60
+ * et présentes dans la réponse normalisée. Utilisé par les UI pour distinguer
61
+ * les modes readonly/edit et afficher des messages contextuels.
62
+ */
63
+ export type AnswerEditDeniedReason = "not_logged_in" | "not_owner" | "form_inactive" | "form_closed";
58
64
  export interface AnswerItemNormalized {
59
65
  id: string;
60
66
  _id: ObjectID;
@@ -64,6 +70,7 @@ export interface AnswerItemNormalized {
64
70
  user?: string | User;
65
71
  links?: AnswerLinksBlock;
66
72
  draft?: boolean;
73
+ finished?: boolean;
67
74
  answers?: Record<string, unknown>;
68
75
  context?: ParentsMap | Record<string, Organization | Project>;
69
76
  form?: string;
@@ -74,6 +81,13 @@ export interface AnswerItemNormalized {
74
81
  date: Date;
75
82
  }>;
76
83
  project?: AnswerProjectRef;
84
+ /**
85
+ * L'utilisateur courant peut-il éditer cette réponse ? Calculé côté backend.
86
+ * Présent par défaut, ou enrichi avec le contexte si `formId` est fourni.
87
+ */
88
+ canEdit?: boolean;
89
+ /** Raison si `canEdit === false`. `null` si `canEdit === true`. */
90
+ editDeniedReason?: AnswerEditDeniedReason | null;
77
91
  [key: string]: unknown;
78
92
  }
79
93
  export {};