@communecter/cocolight-api-client 1.0.132 → 1.0.134

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.
@@ -79,6 +79,7 @@ type BadgeInput = { id: string } | Record<string, any>;
79
79
  type NewsInput = { id: string } | Record<string, any>;
80
80
  type ActionInput = { id: string } | Record<string, any>;
81
81
  type FormInput = { id: string } | Record<string, any>;
82
+ type AnswerInput = { id: string } | Record<string, any>;
82
83
 
83
84
  /**
84
85
  * On force le type de l'import comme une fabrique qui renvoie un objet
@@ -163,7 +164,7 @@ interface EntityTypeMap {
163
164
  }
164
165
 
165
166
  // Types pour les streams et uploads
166
- type ReadableWithMeta = import("stream").Readable & { path?: string, mimeType?: string };
167
+ type ReadableWithMeta = import("stream").Readable & { path?: string, mimeType?: string, size?: number };
167
168
  type UploadInput = File | Blob | Buffer | import("stream").Readable;
168
169
  type ValidatedUpload = File | Buffer | ReadableWithMeta;
169
170
 
@@ -1156,6 +1157,74 @@ export class BaseEntity<TServerData = any> {
1156
1157
  return file;
1157
1158
  }
1158
1159
 
1160
+ /**
1161
+ * Prépare un fichier pour un upload multipart **sans restriction MIME**.
1162
+ *
1163
+ * Diffère de `_validateImage` / `_validateFile` (liste MIME blanche stricte) :
1164
+ * accepte tout MIME et garantit la conversion `Buffer → annotated stream` en
1165
+ * Node (le multipart encoder du package `form-data` ignore les Buffer bruts ;
1166
+ * il a besoin de `path` + `mimeType` sur un Readable).
1167
+ *
1168
+ * Pensé pour les endpoints d'upload large-spectre (ex: coform où l'utilisateur
1169
+ * peut uploader PNG, GIF, WebP, PDF, DOCX, XLSX, etc. — c'est le backend qui
1170
+ * filtre selon la config du Form, pas le client).
1171
+ *
1172
+ * Compatible browser + Node :
1173
+ * - Browser `File` → pass-through
1174
+ * - Browser `Blob` → wrap en `File` (génère nom + extension via MIME)
1175
+ * - Node `Buffer` → détecte MIME via `file-type`, convertit en `Readable`
1176
+ * annoté (`path`, `mimeType`)
1177
+ * - Node `Readable` → pass-through si annoté, sinon utilise `fallbackName`
1178
+ *
1179
+ * @param input - Fichier à uploader (Buffer/File/Blob/Readable)
1180
+ * @param fallbackName - Nom à utiliser si l'input n'en porte pas. Défaut `upload-<ts>`.
1181
+ * @returns `{ qqfile, qqfilename, qqtotalfilesize }` prêt pour multipart.
1182
+ * @throws {ApiError} si le type de l'input n'est pas reconnu.
1183
+ *
1184
+ * @protected
1185
+ */
1186
+ protected async _prepareUploadFile(
1187
+ input: UploadInput,
1188
+ fallbackName: string = `upload-${Date.now()}`,
1189
+ ): Promise<{ qqfile: ValidatedUpload; qqfilename: string; qqtotalfilesize: number }> {
1190
+ const isNode = typeof window === "undefined" && typeof process !== "undefined";
1191
+
1192
+ // Browser : File natif
1193
+ if (typeof File !== "undefined" && input instanceof File) {
1194
+ return { qqfile: input, qqfilename: input.name, qqtotalfilesize: input.size };
1195
+ }
1196
+
1197
+ // Browser : Blob → wrap en File
1198
+ if (typeof Blob !== "undefined" && input instanceof Blob) {
1199
+ const ext = input.type.split("/")[1] || "bin";
1200
+ const name = `${fallbackName}.${ext}`;
1201
+ const file = new File([input], name, { type: input.type });
1202
+ return { qqfile: file, qqfilename: name, qqtotalfilesize: file.size };
1203
+ }
1204
+
1205
+ // Node : Buffer → annotated stream (file-type pour MIME)
1206
+ if (isNode && Buffer.isBuffer(input)) {
1207
+ const ft = await fromBuffer(input);
1208
+ const mime = ft?.mime ?? "application/octet-stream";
1209
+ const ext = ft?.ext ?? "bin";
1210
+ const name = /\.[a-z0-9]+$/i.test(fallbackName) ? fallbackName : `${fallbackName}.${ext}`;
1211
+ const stream = await this._createReadStreamFromBuffer(input, name, mime);
1212
+ return { qqfile: stream, qqfilename: name, qqtotalfilesize: input.length };
1213
+ }
1214
+
1215
+ // Node : Readable déjà annoté (ex: fs.createReadStream)
1216
+ if (isNode && input && typeof (input as { pipe?: unknown }).pipe === "function") {
1217
+ const stream = input as ReadableWithMeta;
1218
+ const name = (typeof stream.path === "string" && stream.path) || fallbackName;
1219
+ // `.size` peut être annoté manuellement par le caller (cf. `fs.statSync(...).size`).
1220
+ // Si absent → 0, à compléter via `opts.qqtotalfilesize` côté méthode appelante.
1221
+ const size = typeof stream.size === "number" ? stream.size : 0;
1222
+ return { qqfile: stream, qqfilename: name, qqtotalfilesize: size };
1223
+ }
1224
+
1225
+ throw new ApiError("Type de fichier non reconnu pour l'upload.", 400);
1226
+ }
1227
+
1159
1228
  /**
1160
1229
  * Valide les entrées d'upload de fichiers.
1161
1230
  *
@@ -1268,14 +1337,17 @@ export class BaseEntity<TServerData = any> {
1268
1337
  }
1269
1338
 
1270
1339
  /**
1271
- * Transforme un Buffer en ReadableStream équivalent à fs.createReadStream
1340
+ * Transforme un Buffer en ReadableStream équivalent à fs.createReadStream.
1341
+ *
1342
+ * Note : passé `protected` car réutilisé par `_prepareUploadFile()` (Answer).
1343
+ *
1272
1344
  * @param buffer - Le buffer contenant les données binaires
1273
1345
  * @param [filename="file.bin"] - Nom de fichier (utilisé dans FormData)
1274
1346
  * @param [mimeType="application/octet-stream"] - Type MIME (utilisé dans FormData)
1275
1347
  * @returns - Readable doté de `path` et `mimeType`
1276
- * @private
1348
+ * @protected
1277
1349
  */
1278
- private async _createReadStreamFromBuffer(buffer: Buffer, filename = "file.bin", mimeType = "application/octet-stream"): Promise<ReadableWithMeta> {
1350
+ protected async _createReadStreamFromBuffer(buffer: Buffer, filename = "file.bin", mimeType = "application/octet-stream"): Promise<ReadableWithMeta> {
1279
1351
  const stream = await this._bufferToReadable(buffer);
1280
1352
  (stream as ReadableWithMeta).path = filename;
1281
1353
  (stream as ReadableWithMeta).mimeType = mimeType;
@@ -3325,6 +3397,24 @@ export class BaseEntity<TServerData = any> {
3325
3397
  return entity as Form;
3326
3398
  }
3327
3399
 
3400
+ /**
3401
+ * Crée une instance d'Answer rattachée à l'entité courante.
3402
+ *
3403
+ * Méthode autorisée uniquement sur `Form` (la seule entité qui porte un
3404
+ * `id` de formulaire pré-rempli pour les drafts). Les autres entités la
3405
+ * bloquent via cet override par défaut qui throw.
3406
+ *
3407
+ * Pour fetch une Answer existante sans contexte Form, passer par
3408
+ * `EntityRegistry.createEntityFromData` ou un endpoint adhoc.
3409
+ *
3410
+ * @param _answerData - `{ id: string }` (fetch) ou objet partiel (draft).
3411
+ * @returns Une Answer.
3412
+ * @throws {ApiError} 501 sur les entités non supportées.
3413
+ */
3414
+ async answer(_answerData: AnswerInput = {}): Promise<Answer> {
3415
+ throw new ApiError(`answer n'existe pas dans ${this.constructor.name}`, 501);
3416
+ }
3417
+
3328
3418
  /**
3329
3419
  * Récupérer les organisations d'une entitée : la liste des organisations dont l'entité est membre ou admin valide.
3330
3420
  * Constant : GET_ORGANIZATIONS_ADMIN | GET_ORGANIZATIONS_NO_ADMIN
package/src/api/Form.ts CHANGED
@@ -119,4 +119,42 @@ export class Form extends BaseEntity<FormItemNormalized> {
119
119
  options
120
120
  );
121
121
  }
122
+
123
+ /**
124
+ * {@inheritDoc BaseEntity#answer}
125
+ *
126
+ * Crée une instance d'Answer **rattachée à ce Form**.
127
+ *
128
+ * - Si `answerData.id` est fourni → fetch l'Answer existante (via `COFORM_ANSWERS_BY_ID`).
129
+ * - Sinon → crée un draft d'Answer avec `form: this.id` pré-rempli, prêt à
130
+ * être complété puis sauvegardé.
131
+ *
132
+ * Le parent de l'Answer renvoyée est ce Form — la chaîne `org > form > answer`
133
+ * est donc préservée, ce qui permet (à terme) à l'Answer de remonter le
134
+ * costumContext via `this.parent.parent` si besoin.
135
+ *
136
+ * @example
137
+ * // Fetch d'une Answer existante
138
+ * const form = await org.form({ id: "6925e2b05dd63b02ca70d6d9" });
139
+ * const answer = await form.answer({ id: "6925869ad76aaf6c5a2b2f8a" });
140
+ *
141
+ * // Création d'un draft
142
+ * const draft = await form.answer();
143
+ * draft.data.answers = { "field1": "value1" };
144
+ * await draft.save();
145
+ */
146
+ override async answer(answerData: Parameters<BaseEntity<FormItemNormalized>["answer"]>[0] = {}): Promise<Answer> {
147
+ if (!this.id) {
148
+ throw new ApiError("Form sans id, impossible de créer une Answer rattachée.", 400);
149
+ }
150
+
151
+ // Pré-remplit `form: this.id` uniquement pour les drafts (pas de fetch par id).
152
+ const hasId = typeof (answerData as { id?: string }).id === "string" && (answerData as { id: string }).id.length > 0;
153
+ const payload: Record<string, any> = hasId
154
+ ? answerData
155
+ : { form: this.id, ...answerData };
156
+
157
+ const entity = await this.entity("answers", payload);
158
+ return entity as Answer;
159
+ }
122
160
  }
@@ -386,11 +386,7 @@ export class Project extends BaseEntity<ProjectItemNormalized> {
386
386
  return super.badge(badgeData);
387
387
  }
388
388
 
389
- /**
390
- * {@inheritDoc BaseEntity#news}
391
- *
392
- * Crée une instance de news et la récupère si nécessaire.
393
- */
389
+
394
390
  /**
395
391
  * {@inheritDoc BaseEntity#form}
396
392
  *
@@ -402,6 +398,11 @@ export class Project extends BaseEntity<ProjectItemNormalized> {
402
398
  return super.form(formData);
403
399
  }
404
400
 
401
+ /**
402
+ * {@inheritDoc BaseEntity#news}
403
+ *
404
+ * Crée une instance de news et la récupère si nécessaire.
405
+ */
405
406
  override async news(newsData: Parameters<BaseEntity<ProjectItemNormalized>["news"]>[0] = {}) {
406
407
  if(!newsData?.id && !this.isContributor()){
407
408
  throw new ApiError("Vous n'avez pas les droits pour créer une news dans ce projet", 403);
@@ -64,6 +64,17 @@ export interface AnswerItemJson {
64
64
  [key: string]: unknown;
65
65
  }
66
66
 
67
+ /**
68
+ * Raisons de refus d'édition côté backend, calculées par `coformAnswersById`
69
+ * et présentes dans la réponse normalisée. Utilisé par les UI pour distinguer
70
+ * les modes readonly/edit et afficher des messages contextuels.
71
+ */
72
+ export type AnswerEditDeniedReason =
73
+ | "not_logged_in"
74
+ | "not_owner"
75
+ | "form_inactive"
76
+ | "form_closed";
77
+
67
78
  export interface AnswerItemNormalized {
68
79
  id: string;
69
80
  _id: ObjectID;
@@ -73,6 +84,7 @@ export interface AnswerItemNormalized {
73
84
  user?: string | User;
74
85
  links?: AnswerLinksBlock;
75
86
  draft?: boolean;
87
+ finished?: boolean;
76
88
  answers?: Record<string, unknown>;
77
89
  context?: ParentsMap | Record<string, Organization | Project>;
78
90
  form?: string;
@@ -83,5 +95,12 @@ export interface AnswerItemNormalized {
83
95
  date: Date;
84
96
  }>;
85
97
  project?: AnswerProjectRef;
98
+ /**
99
+ * L'utilisateur courant peut-il éditer cette réponse ? Calculé côté backend.
100
+ * Présent par défaut, ou enrichi avec le contexte si `formId` est fourni.
101
+ */
102
+ canEdit?: boolean;
103
+ /** Raison si `canEdit === false`. `null` si `canEdit === true`. */
104
+ editDeniedReason?: AnswerEditDeniedReason | null;
86
105
  [key: string]: unknown;
87
106
  }
@@ -1,12 +1,140 @@
1
1
  import { BaseEntity } from "./BaseEntity.js";
2
2
  import type { AnswerItemNormalized } from "./serverDataType/Answer.js";
3
+ import type { FormItemNormalized } from "./serverDataType/Form.js";
4
+ type UploadInput = File | Blob | Buffer | import("stream").Readable;
5
+ /**
6
+ * Format des données utilisateur d'un CoForm — `{ subFormId: { inputId: value } }`.
7
+ * Générique : la lib ne fait aucune supposition sur le type des valeurs.
8
+ */
9
+ export type AllStepsData = Record<string, Record<string, unknown>>;
10
+ /**
11
+ * Valeur d'un upload en attente — objet `{ name?, data: "data:...;base64,..." }`
12
+ * inséré par les inputs uploader avant le pré-upload backend.
13
+ */
14
+ export interface PendingUploadValue {
15
+ name?: string;
16
+ data: string;
17
+ }
18
+ /**
19
+ * Un pending upload détecté dans la structure `allData` — chemin imbriqué +
20
+ * type d'input dérivé du schéma (`formData.inputs[subFormId].inputs[inputId].type`).
21
+ */
22
+ export interface PendingUpload {
23
+ value: PendingUploadValue;
24
+ path: (string | number)[];
25
+ inputType?: string;
26
+ }
27
+ /**
28
+ * Options de `Answer.processUploads()`.
29
+ */
30
+ export interface ProcessUploadsOptions {
31
+ /**
32
+ * Schéma du Form (pour déduire `inputType` des champs uploader/simpleTable).
33
+ * Auto-récupéré via `this.parent.serverData` si parent est un Form.
34
+ */
35
+ formData?: FormItemNormalized;
36
+ /** Nombre d'uploads parallèles par batch. Défaut : 4. */
37
+ batchSize?: number;
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
+ }
58
+ /**
59
+ * Options pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
60
+ * défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
61
+ */
62
+ export interface UploadAnswerFileOptions {
63
+ /** Type de document : `image` (défaut) ou `file` (PDF, DOCX, etc.). */
64
+ docType?: "image" | "file";
65
+ /** Clé de contenu côté backend. `"slider"` (défaut) pour images, `"presentation"` pour inputs uploader. */
66
+ contentKey?: string;
67
+ /** Sous-clé optionnelle (ex: `"subFormId.inputId"` pour les inputs `*.uploader`). */
68
+ subKey?: string;
69
+ /** UUID client (compat fine-uploader). Auto-généré si absent. */
70
+ qquuid?: string;
71
+ /** Nom du fichier. Auto-déduit (File.name, stream.path, ou fallback) si absent. */
72
+ qqfilename?: string;
73
+ /**
74
+ * Taille du fichier en octets. Auto-déduite pour `File` (`.size`), `Blob`
75
+ * (`.size`), `Buffer` (`.length`), ou `Readable` annoté (`.size`).
76
+ *
77
+ * **À fournir explicitement pour un `Readable` non annoté** (ex:
78
+ * `fs.createReadStream(path)` → faire `fs.statSync(path).size` au préalable),
79
+ * sinon `0` est envoyé et le backend peut rejeter le multipart.
80
+ */
81
+ qqtotalfilesize?: number;
82
+ }
83
+ /**
84
+ * Retour normalisé de `Answer.uploadFile()`.
85
+ */
86
+ export interface UploadAnswerFileResult {
87
+ /** ID du document MongoDB (à stocker dans `{docId, docPath}` pour les inputs uploader). */
88
+ docId: string;
89
+ /** Chemin du document. URL absolue côté backend, à nettoyer en chemin relatif si besoin (cf. `cleanUploaderUrls` site-json). */
90
+ docPath: string;
91
+ /** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
92
+ answerId: string;
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
+ }
3
126
  export declare class Answer extends BaseEntity<AnswerItemNormalized> {
4
127
  static entityType: string;
5
128
  static entityTag: string;
6
129
  static SCHEMA_CONSTANTS: string[];
7
- static ADD_BLOCKS: Map<string, string>;
8
- static UPDATE_BLOCKS: Map<string, string>;
130
+ static ADD_BLOCKS: Map<"SAVE_COFORM_ANSWER", "saveCoformAnswer">;
131
+ static UPDATE_BLOCKS: Map<"SAVE_COFORM_ANSWER", "saveCoformAnswer">;
9
132
  defaultFields: Record<string, any>;
133
+ /**
134
+ * Champs `serverData` à ne pas renvoyer au serveur dans le payload de save.
135
+ * `answers`, `addedOptions`, `links`, `form` sont conservés (envoyés au serveur).
136
+ * Les autres sont calculés/posés par le backend.
137
+ */
10
138
  removeFields: string[];
11
139
  /**
12
140
  * Transforme les champs imbriqués (user, context etc.) en instances d'entités.
@@ -16,9 +144,291 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
16
144
  */
17
145
  protected _transformServerData(data: AnswerItemNormalized): AnswerItemNormalized;
18
146
  /**
19
- * Rafraîchit les données de l'entité depuis l'API.
20
- * Constant : COFORM_ANSWERS_BY_ID
21
- */
22
- get(): Promise<Record<string, any>>;
147
+ * Rafraîchit les données de l'entité depuis l'API (`COFORM_ANSWERS_BY_ID`).
148
+ *
149
+ * **Auto-injection du `formId`** : si le parent est un `Form` (cas typique
150
+ * `form.answer({id})`), le param `formId` est automatiquement envoyé pour
151
+ * que le backend calcule `canEdit` et `editDeniedReason` dans la réponse.
152
+ * Le caller peut override via `opts.formId` ou couper avec `opts.formId: ""`.
153
+ *
154
+ * @param opts - Params optionnels :
155
+ * - `fields` : filtrage des champs côté backend
156
+ * - `formId` : override de l'auto-injection (utile pour disable ou forcer un autre form)
157
+ * - `finderPath` : chemin Finder (rare)
158
+ *
159
+ * @example
160
+ * // Cas commun : form.answer({id}) → get() auto avec formId
161
+ * const answer = await form.answer({ id: answerId });
162
+ * answer.serverData.canEdit; // true/false calculé backend
163
+ * answer.serverData.editDeniedReason; // raison si false
164
+ *
165
+ * @example
166
+ * // Cas avancé : filtrage de champs pour optim perf
167
+ * const answer = await form.answer();
168
+ * answer._id(answerId);
169
+ * await answer.get({ fields: ["answers", "canEdit", "editDeniedReason"] });
170
+ */
171
+ get(opts?: AnswerGetOptions): Promise<Record<string, any>>;
172
+ /**
173
+ * Récupère l'ID du parent Form si applicable, pour l'auto-injection dans `get()`.
174
+ * @private
175
+ */
176
+ private _autoFormIdFromParent;
23
177
  form(): Promise<never>;
178
+ /**
179
+ * Supprime cette Answer côté serveur via `DELETE_ELEMENT`
180
+ * (`type=answers`, `id=this.id`).
181
+ *
182
+ * Après succès :
183
+ * - `serverData` et `draftData` sont vidés (sans casser la réactivité)
184
+ * - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
185
+ *
186
+ * @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
187
+ * @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
188
+ *
189
+ * @example
190
+ * const answer = await form.answer({ id: "..." });
191
+ * await answer.delete();
192
+ * answer._isDeleted; // true
193
+ */
194
+ delete(reason?: string): Promise<void>;
195
+ /**
196
+ * Récupère la liste des fichiers attachés à un input précis de cette Answer
197
+ * via `COFORM_GET_ANSWER_FILES`.
198
+ *
199
+ * **Cas d'usage principal** : récupérer les fichiers d'inputs uploader stockés
200
+ * en **format legacy** `{updateDate: [...]}` (sans la clé `files`). Le backend
201
+ * recherche alors les documents MongoDB liés à `{answerId, subKey, docType}`.
202
+ *
203
+ * Pour les Answer en format moderne `{updateDate, files: {<docId>: <path>}}`,
204
+ * les fichiers sont déjà dans `answer.serverData.answers[subFormId][inputId].files`
205
+ * — pas besoin d'appel réseau.
206
+ *
207
+ * @param opts - `subKey` (requis, ex: `"subFormId.inputId"`), `docType`, `contentKey`
208
+ * @returns Liste normalisée des fichiers `{docId, docPath, name?, size?, imagePath?, imageThumbPath?, imageMediumPath?}`.
209
+ * Tableau vide si aucun fichier trouvé.
210
+ * @throws {ApiError} 400 si pas d'id Answer. Throws aussi si le backend renvoie `result: false`.
211
+ *
212
+ * @example
213
+ * const form = await org.form({ id: formId });
214
+ * const answer = await form.answer({ id: answerId });
215
+ * const files = await answer.getFiles({
216
+ * subKey: `${subFormId}.${inputId}`,
217
+ * docType: "image",
218
+ * });
219
+ * files[0].docId; // ID du document (pour suppression future)
220
+ * files[0].docPath; // Chemin relatif (à concaténer avec baseURL pour affichage)
221
+ */
222
+ getFiles(opts: GetFilesOptions): Promise<AnswerFileItem[]>;
223
+ /**
224
+ * Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
225
+ *
226
+ * **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
227
+ * renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
228
+ * — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
229
+ * en update.
230
+ *
231
+ * **Cas uploads suivants (Answer avec id)** : `answerId: this.id` est envoyé.
232
+ *
233
+ * Compatible browser (File/Blob) et Node (Buffer/Readable) via
234
+ * `BaseEntity._prepareUploadFile()` — le multipart encoder du package
235
+ * `form-data` (utilisé en Node) ignore les Buffer bruts, donc conversion en
236
+ * stream annoté.
237
+ *
238
+ * @param file - Fichier à uploader (File/Blob côté browser, Buffer/Readable côté Node)
239
+ * @param opts - `docType`, `contentKey`, `subKey`, `qquuid`, `qqfilename`
240
+ * @returns `{ docId, docPath, answerId }` — toujours fournis par le backend.
241
+ * @throws {ApiError} si `formId` introuvable, ou réponse invalide.
242
+ *
243
+ * @example
244
+ * // Browser (depuis un <input type="file">)
245
+ * const form = await org.form({ id: formId });
246
+ * const answer = await form.answer(); // draft
247
+ * const { docId, docPath } = await answer.uploadFile(htmlFile, {
248
+ * contentKey: "presentation",
249
+ * subKey: `${subFormId}.${inputId}`,
250
+ * });
251
+ * // answer.id désormais défini → uploads suivants utilisent cet id.
252
+ *
253
+ * @example
254
+ * // Node (depuis un Buffer)
255
+ * const buffer = await fs.promises.readFile("./photo.jpg");
256
+ * const result = await answer.uploadFile(buffer, { docType: "image" });
257
+ */
258
+ uploadFile(file: UploadInput, opts?: UploadAnswerFileOptions): Promise<UploadAnswerFileResult>;
259
+ /**
260
+ * Orchestre l'upload de tous les fichiers pendants (`data:URI`) trouvés dans
261
+ * `allData`, puis renvoie la structure transformée prête pour `answer.save()`.
262
+ *
263
+ * **Pipeline complet** :
264
+ * 1. Walk récursif de `allData` → collecte les `data:URI` avec leur path + inputType
265
+ * 2. Premier upload (si Answer sans id) → backend crée l'Answer + pose `this.id`
266
+ * 3. Uploads restants en batches parallèles (défaut 4)
267
+ * 4. Remplacement dans la structure :
268
+ * - inputs `*.uploader` → `{ docId, docPath }`
269
+ * - autres → `docPath` string
270
+ * 5. Normalisation legacy uploader : `Array<{docId, docPath}>` → `{ updateDate, files }`
271
+ * 6. Nettoyage URLs absolues → chemins relatifs (uploader/simpleTable uniquement)
272
+ *
273
+ * Ne touche PAS `addedOptions` ni `links` — caller les pose séparément avant `save()`.
274
+ *
275
+ * @param allData - Données utilisateur `{ subFormId: { inputId: value } }`
276
+ * @param options - `formData` (auto via parent Form), `batchSize` (défaut 4)
277
+ * @returns `allData` transformé, prêt à être affecté à `answer.data.answers`
278
+ *
279
+ * @example
280
+ * const form = await org.form({ id: formId });
281
+ * const answer = await form.answer(); // ou form.answer({ id }) en édition
282
+ *
283
+ * const prepared = await answer.processUploads(allStepsDataFromForm);
284
+ * answer.data.answers = prepared;
285
+ * answer.data.addedOptions = addedOptions; // optionnel
286
+ * answer.data.links = finderLinks; // optionnel
287
+ * await answer.save();
288
+ */
289
+ processUploads(allData: AllStepsData, options?: ProcessUploadsOptions): Promise<AllStepsData>;
290
+ /**
291
+ * UUID v4 compatible browser + Node.
292
+ * Utilise `crypto.randomUUID()` si dispo, sinon fallback Math.random.
293
+ * @private
294
+ */
295
+ private _generateUuid;
296
+ /**
297
+ * Sauvegarde une nouvelle Answer (sans `id`) via `SAVE_COFORM_ANSWER`.
298
+ *
299
+ * Le payload caller-side attendu (dans `draftData`) :
300
+ * - `answers` (requis) — objet `{ subFormId: { inputId: value } }`
301
+ * - `form` ou parent Form (requis indirectement) — pour résoudre `formId`
302
+ * - `addedOptions` (optionnel) — multiCheckboxPlus dynamiques
303
+ * - `links` (optionnel) — sélections Finder à attacher à l'answer
304
+ *
305
+ * Sérialise automatiquement `answers`/`addedOptions`/`links` en JSON
306
+ * (le wire-format `application/x-www-form-urlencoded` exige des strings).
307
+ *
308
+ * Après succès, l'ID retourné par le serveur est posé dans `_draftData.id`
309
+ * → le `refresh()` de `BaseEntity.save()` peut alors récupérer l'Answer canonique.
310
+ *
311
+ * @protected — utiliser `answer.save()` (BaseEntity dispatch automatiquement).
312
+ */
313
+ _add: (payload: Record<string, any>) => Promise<void>;
314
+ /**
315
+ * Met à jour une Answer existante via `SAVE_COFORM_ANSWER` (avec `answerId`).
316
+ *
317
+ * Si `payload.answers` est `undefined` (le caller a modifié uniquement
318
+ * `addedOptions` ou `links`), on retombe sur `serverData.answers` pour ne
319
+ * pas envoyer un payload vide qui écraserait les réponses existantes.
320
+ *
321
+ * @protected — utiliser `answer.save()`.
322
+ */
323
+ _update: (payload: Record<string, any>) => Promise<boolean>;
324
+ /**
325
+ * Résout le `formId` à envoyer à `saveCoformAnswer`.
326
+ *
327
+ * Priorité :
328
+ * 1. `payload.form` (posé via `draftData.form` par `form.answer()`)
329
+ * 2. `serverData.form` (Answer chargée via `get()`)
330
+ * 3. `this.parent.id` si le parent est un `Form`
331
+ *
332
+ * @throws {ApiError} 400 si aucune source ne fournit un formId valide.
333
+ * @private
334
+ */
335
+ private _resolveFormId;
336
+ /**
337
+ * Construit le payload `SaveCoformAnswerData` en sérialisant les champs JSON
338
+ * requis par le wire-format `application/x-www-form-urlencoded`.
339
+ *
340
+ * Si `payload.answers` est absent en update, retombe sur `serverData.answers`
341
+ * (cf. design point 6.5 — indulgent envers le caller).
342
+ *
343
+ * @private
344
+ */
345
+ private _buildSavePayload;
346
+ /**
347
+ * Récupère `formData` (schéma du Form) depuis le parent si c'est un Form.
348
+ * Renvoie `undefined` si pas exploitable — `processUploads` continuera mais
349
+ * sans détection automatique d'`inputType` (pas de normalisation uploader,
350
+ * pas de clean URLs).
351
+ * @private
352
+ */
353
+ private _resolveFormData;
354
+ private _isObjectRecord;
355
+ private _isDataUri;
356
+ private _isPendingUploadValue;
357
+ private _parseMimeType;
358
+ private _inferExtensionFromMimeType;
359
+ private _sanitizeBaseName;
360
+ /**
361
+ * Convertit un `PendingUploadValue` (data:URI) en `{file, docType, filename}`
362
+ * exploitable par `uploadFile()`. Compatible browser + Node :
363
+ * - Browser → `File` natif
364
+ * - Node → `Buffer` (sera converti en stream par `_prepareUploadFile()`)
365
+ * @private
366
+ */
367
+ private _dataUriToFile;
368
+ /**
369
+ * Parcourt récursivement `allData` pour trouver tous les `data:URI` à uploader.
370
+ * Renvoie chaque pending avec son `path` (chemin imbriqué) et `inputType`
371
+ * (déduit du schéma `formData` si fourni).
372
+ * @private
373
+ */
374
+ private _collectPendingUploads;
375
+ /**
376
+ * Détermine `contentKey` et `subKey` pour un upload selon le type d'input :
377
+ * - `*.uploader` → `presentation` + subKey `"subFormId.inputId"`
378
+ * - autres → `slider` sans subKey
379
+ * @private
380
+ */
381
+ private _getUploadKeys;
382
+ private _getValueAtPath;
383
+ private _setValueAtPath;
384
+ /**
385
+ * Walk les `answers[subFormId][inputId]` pour normaliser les valeurs des
386
+ * inputs `*.uploader` vers le format canonique `{updateDate, files: {docId: docPath}}`.
387
+ *
388
+ * Utilisé par `_transformServerData()` pour harmoniser les Answer en format
389
+ * legacy Array `[{docId, docPath}]` avec le format moderne (ce que
390
+ * `processUploads()` écrit).
391
+ *
392
+ * Skip si le schéma `formData` n'a pas l'inputType pour un champ donné.
393
+ * @private
394
+ */
395
+ private _normalizeLegacyUploaderInAnswers;
396
+ /**
397
+ * Normalise une valeur d'input uploader vers le format legacy backend :
398
+ * `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
399
+ * @private
400
+ */
401
+ private _normalizeUploaderValue;
402
+ private _cleanUrlToRelativePath;
403
+ /**
404
+ * Suffixes de types d'inputs dont les valeurs contiennent des URLs de fichiers
405
+ * à nettoyer avant envoi au serveur.
406
+ * @private
407
+ */
408
+ private static readonly INPUT_TYPES_TO_CLEAN_URLS;
409
+ /**
410
+ * Nettoie les URLs absolues → chemins relatifs **uniquement** sur les champs
411
+ * uploader/simpleTable. Les autres champs (finder, text…) peuvent contenir
412
+ * des URLs légitimes qu'il ne faut pas tronquer.
413
+ * @private
414
+ */
415
+ private _cleanUploaderUrls;
416
+ /**
417
+ * Upload par batches parallèles (défaut : 4 fichiers simultanés).
418
+ * @private
419
+ */
420
+ private _uploadInBatches;
421
+ /**
422
+ * Upload un PendingUpload, retourne la valeur de remplacement à insérer
423
+ * dans `allData` (`{docId, docPath}` pour uploader, `docPath` string sinon).
424
+ * @private
425
+ */
426
+ private _uploadPendingItem;
427
+ /**
428
+ * Upload un PendingUpload + remplace immédiatement dans `prepared` à son path.
429
+ * Utilisé pour le premier upload (séquentiel) qui crée l'Answer.
430
+ * @private
431
+ */
432
+ private _uploadAndReplace;
24
433
  }
434
+ export {};