@communecter/cocolight-api-client 1.0.132 → 1.0.133

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);
@@ -1,12 +1,89 @@
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 pour `Answer.uploadFile()`. Tous les champs sont optionnels — les
41
+ * défauts reproduisent les valeurs backend (`docType: "image"`, `contentKey: "slider"`).
42
+ */
43
+ export interface UploadAnswerFileOptions {
44
+ /** Type de document : `image` (défaut) ou `file` (PDF, DOCX, etc.). */
45
+ docType?: "image" | "file";
46
+ /** Clé de contenu côté backend. `"slider"` (défaut) pour images, `"presentation"` pour inputs uploader. */
47
+ contentKey?: string;
48
+ /** Sous-clé optionnelle (ex: `"subFormId.inputId"` pour les inputs `*.uploader`). */
49
+ subKey?: string;
50
+ /** UUID client (compat fine-uploader). Auto-généré si absent. */
51
+ qquuid?: string;
52
+ /** Nom du fichier. Auto-déduit (File.name, stream.path, ou fallback) si absent. */
53
+ qqfilename?: string;
54
+ /**
55
+ * Taille du fichier en octets. Auto-déduite pour `File` (`.size`), `Blob`
56
+ * (`.size`), `Buffer` (`.length`), ou `Readable` annoté (`.size`).
57
+ *
58
+ * **À fournir explicitement pour un `Readable` non annoté** (ex:
59
+ * `fs.createReadStream(path)` → faire `fs.statSync(path).size` au préalable),
60
+ * sinon `0` est envoyé et le backend peut rejeter le multipart.
61
+ */
62
+ qqtotalfilesize?: number;
63
+ }
64
+ /**
65
+ * Retour normalisé de `Answer.uploadFile()`.
66
+ */
67
+ export interface UploadAnswerFileResult {
68
+ /** ID du document MongoDB (à stocker dans `{docId, docPath}` pour les inputs uploader). */
69
+ docId: string;
70
+ /** Chemin du document. URL absolue côté backend, à nettoyer en chemin relatif si besoin (cf. `cleanUploaderUrls` site-json). */
71
+ docPath: string;
72
+ /** ID de l'Answer (créée par cet upload si premier, sinon `= this.id`). */
73
+ answerId: string;
74
+ }
3
75
  export declare class Answer extends BaseEntity<AnswerItemNormalized> {
4
76
  static entityType: string;
5
77
  static entityTag: string;
6
78
  static SCHEMA_CONSTANTS: string[];
7
- static ADD_BLOCKS: Map<string, string>;
8
- static UPDATE_BLOCKS: Map<string, string>;
79
+ static ADD_BLOCKS: Map<"SAVE_COFORM_ANSWER", "saveCoformAnswer">;
80
+ static UPDATE_BLOCKS: Map<"SAVE_COFORM_ANSWER", "saveCoformAnswer">;
9
81
  defaultFields: Record<string, any>;
82
+ /**
83
+ * Champs `serverData` à ne pas renvoyer au serveur dans le payload de save.
84
+ * `answers`, `addedOptions`, `links`, `form` sont conservés (envoyés au serveur).
85
+ * Les autres sont calculés/posés par le backend.
86
+ */
10
87
  removeFields: string[];
11
88
  /**
12
89
  * Transforme les champs imbriqués (user, context etc.) en instances d'entités.
@@ -21,4 +98,220 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
21
98
  */
22
99
  get(): Promise<Record<string, any>>;
23
100
  form(): Promise<never>;
101
+ /**
102
+ * Supprime cette Answer côté serveur via `DELETE_ELEMENT`
103
+ * (`type=answers`, `id=this.id`).
104
+ *
105
+ * Après succès :
106
+ * - `serverData` et `draftData` sont vidés (sans casser la réactivité)
107
+ * - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
108
+ *
109
+ * @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
110
+ * @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
111
+ *
112
+ * @example
113
+ * const answer = await form.answer({ id: "..." });
114
+ * await answer.delete();
115
+ * answer._isDeleted; // true
116
+ */
117
+ delete(reason?: string): Promise<void>;
118
+ /**
119
+ * Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
120
+ *
121
+ * **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
122
+ * renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
123
+ * — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
124
+ * en update.
125
+ *
126
+ * **Cas uploads suivants (Answer avec id)** : `answerId: this.id` est envoyé.
127
+ *
128
+ * Compatible browser (File/Blob) et Node (Buffer/Readable) via
129
+ * `BaseEntity._prepareUploadFile()` — le multipart encoder du package
130
+ * `form-data` (utilisé en Node) ignore les Buffer bruts, donc conversion en
131
+ * stream annoté.
132
+ *
133
+ * @param file - Fichier à uploader (File/Blob côté browser, Buffer/Readable côté Node)
134
+ * @param opts - `docType`, `contentKey`, `subKey`, `qquuid`, `qqfilename`
135
+ * @returns `{ docId, docPath, answerId }` — toujours fournis par le backend.
136
+ * @throws {ApiError} si `formId` introuvable, ou réponse invalide.
137
+ *
138
+ * @example
139
+ * // Browser (depuis un <input type="file">)
140
+ * const form = await org.form({ id: formId });
141
+ * const answer = await form.answer(); // draft
142
+ * const { docId, docPath } = await answer.uploadFile(htmlFile, {
143
+ * contentKey: "presentation",
144
+ * subKey: `${subFormId}.${inputId}`,
145
+ * });
146
+ * // answer.id désormais défini → uploads suivants utilisent cet id.
147
+ *
148
+ * @example
149
+ * // Node (depuis un Buffer)
150
+ * const buffer = await fs.promises.readFile("./photo.jpg");
151
+ * const result = await answer.uploadFile(buffer, { docType: "image" });
152
+ */
153
+ uploadFile(file: UploadInput, opts?: UploadAnswerFileOptions): Promise<UploadAnswerFileResult>;
154
+ /**
155
+ * Orchestre l'upload de tous les fichiers pendants (`data:URI`) trouvés dans
156
+ * `allData`, puis renvoie la structure transformée prête pour `answer.save()`.
157
+ *
158
+ * **Pipeline complet** :
159
+ * 1. Walk récursif de `allData` → collecte les `data:URI` avec leur path + inputType
160
+ * 2. Premier upload (si Answer sans id) → backend crée l'Answer + pose `this.id`
161
+ * 3. Uploads restants en batches parallèles (défaut 4)
162
+ * 4. Remplacement dans la structure :
163
+ * - inputs `*.uploader` → `{ docId, docPath }`
164
+ * - autres → `docPath` string
165
+ * 5. Normalisation legacy uploader : `Array<{docId, docPath}>` → `{ updateDate, files }`
166
+ * 6. Nettoyage URLs absolues → chemins relatifs (uploader/simpleTable uniquement)
167
+ *
168
+ * Ne touche PAS `addedOptions` ni `links` — caller les pose séparément avant `save()`.
169
+ *
170
+ * @param allData - Données utilisateur `{ subFormId: { inputId: value } }`
171
+ * @param options - `formData` (auto via parent Form), `batchSize` (défaut 4)
172
+ * @returns `allData` transformé, prêt à être affecté à `answer.data.answers`
173
+ *
174
+ * @example
175
+ * const form = await org.form({ id: formId });
176
+ * const answer = await form.answer(); // ou form.answer({ id }) en édition
177
+ *
178
+ * const prepared = await answer.processUploads(allStepsDataFromForm);
179
+ * answer.data.answers = prepared;
180
+ * answer.data.addedOptions = addedOptions; // optionnel
181
+ * answer.data.links = finderLinks; // optionnel
182
+ * await answer.save();
183
+ */
184
+ processUploads(allData: AllStepsData, options?: ProcessUploadsOptions): Promise<AllStepsData>;
185
+ /**
186
+ * UUID v4 compatible browser + Node.
187
+ * Utilise `crypto.randomUUID()` si dispo, sinon fallback Math.random.
188
+ * @private
189
+ */
190
+ private _generateUuid;
191
+ /**
192
+ * Sauvegarde une nouvelle Answer (sans `id`) via `SAVE_COFORM_ANSWER`.
193
+ *
194
+ * Le payload caller-side attendu (dans `draftData`) :
195
+ * - `answers` (requis) — objet `{ subFormId: { inputId: value } }`
196
+ * - `form` ou parent Form (requis indirectement) — pour résoudre `formId`
197
+ * - `addedOptions` (optionnel) — multiCheckboxPlus dynamiques
198
+ * - `links` (optionnel) — sélections Finder à attacher à l'answer
199
+ *
200
+ * Sérialise automatiquement `answers`/`addedOptions`/`links` en JSON
201
+ * (le wire-format `application/x-www-form-urlencoded` exige des strings).
202
+ *
203
+ * Après succès, l'ID retourné par le serveur est posé dans `_draftData.id`
204
+ * → le `refresh()` de `BaseEntity.save()` peut alors récupérer l'Answer canonique.
205
+ *
206
+ * @protected — utiliser `answer.save()` (BaseEntity dispatch automatiquement).
207
+ */
208
+ _add: (payload: Record<string, any>) => Promise<void>;
209
+ /**
210
+ * Met à jour une Answer existante via `SAVE_COFORM_ANSWER` (avec `answerId`).
211
+ *
212
+ * Si `payload.answers` est `undefined` (le caller a modifié uniquement
213
+ * `addedOptions` ou `links`), on retombe sur `serverData.answers` pour ne
214
+ * pas envoyer un payload vide qui écraserait les réponses existantes.
215
+ *
216
+ * @protected — utiliser `answer.save()`.
217
+ */
218
+ _update: (payload: Record<string, any>) => Promise<boolean>;
219
+ /**
220
+ * Résout le `formId` à envoyer à `saveCoformAnswer`.
221
+ *
222
+ * Priorité :
223
+ * 1. `payload.form` (posé via `draftData.form` par `form.answer()`)
224
+ * 2. `serverData.form` (Answer chargée via `get()`)
225
+ * 3. `this.parent.id` si le parent est un `Form`
226
+ *
227
+ * @throws {ApiError} 400 si aucune source ne fournit un formId valide.
228
+ * @private
229
+ */
230
+ private _resolveFormId;
231
+ /**
232
+ * Construit le payload `SaveCoformAnswerData` en sérialisant les champs JSON
233
+ * requis par le wire-format `application/x-www-form-urlencoded`.
234
+ *
235
+ * Si `payload.answers` est absent en update, retombe sur `serverData.answers`
236
+ * (cf. design point 6.5 — indulgent envers le caller).
237
+ *
238
+ * @private
239
+ */
240
+ private _buildSavePayload;
241
+ /**
242
+ * Récupère `formData` (schéma du Form) depuis le parent si c'est un Form.
243
+ * Renvoie `undefined` si pas exploitable — `processUploads` continuera mais
244
+ * sans détection automatique d'`inputType` (pas de normalisation uploader,
245
+ * pas de clean URLs).
246
+ * @private
247
+ */
248
+ private _resolveFormData;
249
+ private _isObjectRecord;
250
+ private _isDataUri;
251
+ private _isPendingUploadValue;
252
+ private _parseMimeType;
253
+ private _inferExtensionFromMimeType;
254
+ private _sanitizeBaseName;
255
+ /**
256
+ * Convertit un `PendingUploadValue` (data:URI) en `{file, docType, filename}`
257
+ * exploitable par `uploadFile()`. Compatible browser + Node :
258
+ * - Browser → `File` natif
259
+ * - Node → `Buffer` (sera converti en stream par `_prepareUploadFile()`)
260
+ * @private
261
+ */
262
+ private _dataUriToFile;
263
+ /**
264
+ * Parcourt récursivement `allData` pour trouver tous les `data:URI` à uploader.
265
+ * Renvoie chaque pending avec son `path` (chemin imbriqué) et `inputType`
266
+ * (déduit du schéma `formData` si fourni).
267
+ * @private
268
+ */
269
+ private _collectPendingUploads;
270
+ /**
271
+ * Détermine `contentKey` et `subKey` pour un upload selon le type d'input :
272
+ * - `*.uploader` → `presentation` + subKey `"subFormId.inputId"`
273
+ * - autres → `slider` sans subKey
274
+ * @private
275
+ */
276
+ private _getUploadKeys;
277
+ private _getValueAtPath;
278
+ private _setValueAtPath;
279
+ /**
280
+ * Normalise une valeur d'input uploader vers le format legacy backend :
281
+ * `{ updateDate: ["DD/MM/YYYY"], files: { "<docId>": "<docPath>", ... } }`
282
+ * @private
283
+ */
284
+ private _normalizeUploaderValue;
285
+ private _cleanUrlToRelativePath;
286
+ /**
287
+ * Suffixes de types d'inputs dont les valeurs contiennent des URLs de fichiers
288
+ * à nettoyer avant envoi au serveur.
289
+ * @private
290
+ */
291
+ private static readonly INPUT_TYPES_TO_CLEAN_URLS;
292
+ /**
293
+ * Nettoie les URLs absolues → chemins relatifs **uniquement** sur les champs
294
+ * uploader/simpleTable. Les autres champs (finder, text…) peuvent contenir
295
+ * des URLs légitimes qu'il ne faut pas tronquer.
296
+ * @private
297
+ */
298
+ private _cleanUploaderUrls;
299
+ /**
300
+ * Upload par batches parallèles (défaut : 4 fichiers simultanés).
301
+ * @private
302
+ */
303
+ private _uploadInBatches;
304
+ /**
305
+ * Upload un PendingUpload, retourne la valeur de remplacement à insérer
306
+ * dans `allData` (`{docId, docPath}` pour uploader, `docPath` string sinon).
307
+ * @private
308
+ */
309
+ private _uploadPendingItem;
310
+ /**
311
+ * Upload un PendingUpload + remplace immédiatement dans `prepared` à son path.
312
+ * Utilisé pour le premier upload (séquentiel) qui crée l'Answer.
313
+ * @private
314
+ */
315
+ private _uploadAndReplace;
24
316
  }
317
+ export {};
@@ -41,6 +41,9 @@ type ActionInput = {
41
41
  type FormInput = {
42
42
  id: string;
43
43
  } | Record<string, any>;
44
+ type AnswerInput = {
45
+ id: string;
46
+ } | Record<string, any>;
44
47
  type ApiClient = import("../ApiClient.js").default;
45
48
  type EndpointApi = import("./EndpointApi.js").default;
46
49
  type User = import("./User.js").User;
@@ -108,6 +111,7 @@ interface EntityTypeMap {
108
111
  type ReadableWithMeta = import("stream").Readable & {
109
112
  path?: string;
110
113
  mimeType?: string;
114
+ size?: number;
111
115
  };
112
116
  type UploadInput = File | Blob | Buffer | import("stream").Readable;
113
117
  type ValidatedUpload = File | Buffer | ReadableWithMeta;
@@ -527,6 +531,37 @@ export declare class BaseEntity<TServerData = any> {
527
531
  * @private
528
532
  */
529
533
  protected _validateFile(fileInput: UploadInput): Promise<ValidatedUpload>;
534
+ /**
535
+ * Prépare un fichier pour un upload multipart **sans restriction MIME**.
536
+ *
537
+ * Diffère de `_validateImage` / `_validateFile` (liste MIME blanche stricte) :
538
+ * accepte tout MIME et garantit la conversion `Buffer → annotated stream` en
539
+ * Node (le multipart encoder du package `form-data` ignore les Buffer bruts ;
540
+ * il a besoin de `path` + `mimeType` sur un Readable).
541
+ *
542
+ * Pensé pour les endpoints d'upload large-spectre (ex: coform où l'utilisateur
543
+ * peut uploader PNG, GIF, WebP, PDF, DOCX, XLSX, etc. — c'est le backend qui
544
+ * filtre selon la config du Form, pas le client).
545
+ *
546
+ * Compatible browser + Node :
547
+ * - Browser `File` → pass-through
548
+ * - Browser `Blob` → wrap en `File` (génère nom + extension via MIME)
549
+ * - Node `Buffer` → détecte MIME via `file-type`, convertit en `Readable`
550
+ * annoté (`path`, `mimeType`)
551
+ * - Node `Readable` → pass-through si annoté, sinon utilise `fallbackName`
552
+ *
553
+ * @param input - Fichier à uploader (Buffer/File/Blob/Readable)
554
+ * @param fallbackName - Nom à utiliser si l'input n'en porte pas. Défaut `upload-<ts>`.
555
+ * @returns `{ qqfile, qqfilename, qqtotalfilesize }` prêt pour multipart.
556
+ * @throws {ApiError} si le type de l'input n'est pas reconnu.
557
+ *
558
+ * @protected
559
+ */
560
+ protected _prepareUploadFile(input: UploadInput, fallbackName?: string): Promise<{
561
+ qqfile: ValidatedUpload;
562
+ qqfilename: string;
563
+ qqtotalfilesize: number;
564
+ }>;
530
565
  /**
531
566
  * Valide les entrées d'upload de fichiers.
532
567
  *
@@ -539,14 +574,17 @@ export declare class BaseEntity<TServerData = any> {
539
574
  */
540
575
  private _validateUploadInput;
541
576
  /**
542
- * Transforme un Buffer en ReadableStream équivalent à fs.createReadStream
577
+ * Transforme un Buffer en ReadableStream équivalent à fs.createReadStream.
578
+ *
579
+ * Note : passé `protected` car réutilisé par `_prepareUploadFile()` (Answer).
580
+ *
543
581
  * @param buffer - Le buffer contenant les données binaires
544
582
  * @param [filename="file.bin"] - Nom de fichier (utilisé dans FormData)
545
583
  * @param [mimeType="application/octet-stream"] - Type MIME (utilisé dans FormData)
546
584
  * @returns - Readable doté de `path` et `mimeType`
547
- * @private
585
+ * @protected
548
586
  */
549
- private _createReadStreamFromBuffer;
587
+ protected _createReadStreamFromBuffer(buffer: Buffer, filename?: string, mimeType?: string): Promise<ReadableWithMeta>;
550
588
  /**
551
589
  * Transforme un Buffer en ReadableStream.
552
590
  *
@@ -1195,6 +1233,21 @@ export declare class BaseEntity<TServerData = any> {
1195
1233
  * const answers = await form.getAnswers(); // utilise le costumContext de l'org
1196
1234
  */
1197
1235
  form(formData?: FormInput): Promise<Form>;
1236
+ /**
1237
+ * Crée une instance d'Answer rattachée à l'entité courante.
1238
+ *
1239
+ * Méthode autorisée uniquement sur `Form` (la seule entité qui porte un
1240
+ * `id` de formulaire pré-rempli pour les drafts). Les autres entités la
1241
+ * bloquent via cet override par défaut qui throw.
1242
+ *
1243
+ * Pour fetch une Answer existante sans contexte Form, passer par
1244
+ * `EntityRegistry.createEntityFromData` ou un endpoint adhoc.
1245
+ *
1246
+ * @param _answerData - `{ id: string }` (fetch) ou objet partiel (draft).
1247
+ * @returns Une Answer.
1248
+ * @throws {ApiError} 501 sur les entités non supportées.
1249
+ */
1250
+ answer(_answerData?: AnswerInput): Promise<Answer>;
1198
1251
  /**
1199
1252
  * Récupérer les organisations d'une entitée : la liste des organisations dont l'entité est membre ou admin valide.
1200
1253
  * Constant : GET_ORGANIZATIONS_ADMIN | GET_ORGANIZATIONS_NO_ADMIN
@@ -54,4 +54,28 @@ export declare class Form extends BaseEntity<FormItemNormalized> {
54
54
  getAnswers(data?: Partial<CoformAnswersSearchData>, options?: {
55
55
  restoredState?: PaginatorState;
56
56
  }): Promise<PaginatorPage<Answer>>;
57
+ /**
58
+ * {@inheritDoc BaseEntity#answer}
59
+ *
60
+ * Crée une instance d'Answer **rattachée à ce Form**.
61
+ *
62
+ * - Si `answerData.id` est fourni → fetch l'Answer existante (via `COFORM_ANSWERS_BY_ID`).
63
+ * - Sinon → crée un draft d'Answer avec `form: this.id` pré-rempli, prêt à
64
+ * être complété puis sauvegardé.
65
+ *
66
+ * Le parent de l'Answer renvoyée est ce Form — la chaîne `org > form > answer`
67
+ * est donc préservée, ce qui permet (à terme) à l'Answer de remonter le
68
+ * costumContext via `this.parent.parent` si besoin.
69
+ *
70
+ * @example
71
+ * // Fetch d'une Answer existante
72
+ * const form = await org.form({ id: "6925e2b05dd63b02ca70d6d9" });
73
+ * const answer = await form.answer({ id: "6925869ad76aaf6c5a2b2f8a" });
74
+ *
75
+ * // Création d'un draft
76
+ * const draft = await form.answer();
77
+ * draft.data.answers = { "field1": "value1" };
78
+ * await draft.save();
79
+ */
80
+ answer(answerData?: Parameters<BaseEntity<FormItemNormalized>["answer"]>[0]): Promise<Answer>;
57
81
  }
@@ -149,11 +149,6 @@ export declare class Project extends BaseEntity<ProjectItemNormalized> {
149
149
  * Crée une instance de badge et la récupère si nécessaire.
150
150
  */
151
151
  badge(badgeData?: Parameters<BaseEntity<ProjectItemNormalized>["badge"]>[0]): Promise<import("./Badge.js").Badge>;
152
- /**
153
- * {@inheritDoc BaseEntity#news}
154
- *
155
- * Crée une instance de news et la récupère si nécessaire.
156
- */
157
152
  /**
158
153
  * {@inheritDoc BaseEntity#form}
159
154
  *
@@ -162,6 +157,11 @@ export declare class Project extends BaseEntity<ProjectItemNormalized> {
162
157
  * `coformAnswersSearch` avec le bon costumContext.
163
158
  */
164
159
  form(formData?: Parameters<BaseEntity<ProjectItemNormalized>["form"]>[0]): Promise<import("./Form.js").Form>;
160
+ /**
161
+ * {@inheritDoc BaseEntity#news}
162
+ *
163
+ * Crée une instance de news et la récupère si nécessaire.
164
+ */
165
165
  news(newsData?: Parameters<BaseEntity<ProjectItemNormalized>["news"]>[0]): Promise<import("./News.js").News>;
166
166
  /**
167
167
  * Vérifie qu'un milestoneId existe dans `serverData.oceco.milestones[]`.