@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.
- package/dist/cocolight-api-client.browser.js +1 -1
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js +1 -1
- package/dist/cocolight-api-client.vite.mjs.js.map +1 -1
- package/package.json +1 -1
- package/src/Api.ts +5 -5
- package/src/api/Answer.ts +842 -5
- package/src/api/BaseEntity.ts +94 -4
- package/src/api/Form.ts +38 -0
- package/src/api/Project.ts +6 -5
- package/types/api/Answer.d.ts +295 -2
- package/types/api/BaseEntity.d.ts +56 -3
- package/types/api/Form.d.ts +24 -0
- package/types/api/Project.d.ts +5 -5
package/src/api/BaseEntity.ts
CHANGED
|
@@ -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
|
-
* @
|
|
1348
|
+
* @protected
|
|
1277
1349
|
*/
|
|
1278
|
-
|
|
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
|
}
|
package/src/api/Project.ts
CHANGED
|
@@ -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);
|
package/types/api/Answer.d.ts
CHANGED
|
@@ -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<
|
|
8
|
-
static UPDATE_BLOCKS: Map<
|
|
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
|
-
* @
|
|
585
|
+
* @protected
|
|
548
586
|
*/
|
|
549
|
-
|
|
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
|
package/types/api/Form.d.ts
CHANGED
|
@@ -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
|
}
|
package/types/api/Project.d.ts
CHANGED
|
@@ -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[]`.
|