@communecter/cocolight-api-client 1.0.134 → 1.0.135
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/Answer.ts +152 -1
- package/src/api/BaseEntity.ts +54 -8
- package/types/api/Answer.d.ts +74 -1
- package/types/api/BaseEntity.d.ts +45 -6
package/package.json
CHANGED
package/src/api/Answer.ts
CHANGED
|
@@ -167,6 +167,79 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
167
167
|
"voteCount", "vote", "project"
|
|
168
168
|
];
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* {@inheritDoc BaseEntity#isAuthor}
|
|
172
|
+
*
|
|
173
|
+
* Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
|
|
174
|
+
* comme la version par défaut de BaseEntity). Le `user` peut être :
|
|
175
|
+
* - un `string` (ID MongoDB brut renvoyé par le backend)
|
|
176
|
+
* - une instance `User` (après `_transformServerData` qui le link en entité)
|
|
177
|
+
*
|
|
178
|
+
* Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
|
|
179
|
+
* la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
|
|
180
|
+
*/
|
|
181
|
+
/**
|
|
182
|
+
* {@inheritDoc BaseEntity#_isAdminViaHierarchy}
|
|
183
|
+
*
|
|
184
|
+
* Override Answer : la hiérarchie d'Answer passe par son **parent instance
|
|
185
|
+
* Form** (pas par `serverData.parent` direct comme pour `events`/`projects`/`poi`).
|
|
186
|
+
*
|
|
187
|
+
* Chaîne : `Answer → parent (Form) → form.serverData.parent ({[orgId]: {type, name}}) → check userContext.links`.
|
|
188
|
+
*
|
|
189
|
+
* Si le parent n'est pas un Form (cas `api.answer({id})` direct sans contexte),
|
|
190
|
+
* renvoie `false` — impossible de vérifier la hiérarchie sans le Form.
|
|
191
|
+
*
|
|
192
|
+
* @protected
|
|
193
|
+
*/
|
|
194
|
+
protected override _isAdminViaHierarchy(): boolean {
|
|
195
|
+
const form = this.parent;
|
|
196
|
+
if (!form || typeof (form as any).getEntityType !== "function"
|
|
197
|
+
|| (form as any).getEntityType() !== "forms") {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const formParents = (form as { serverData?: { parent?: Record<string, unknown> } }).serverData?.parent;
|
|
202
|
+
if (!formParents || typeof formParents !== "object") {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const [parentId, parentRef] of Object.entries(formParents)) {
|
|
207
|
+
if (!parentRef || typeof parentRef !== "object") continue;
|
|
208
|
+
const ref = parentRef as { type?: string };
|
|
209
|
+
if (!ref.type) continue;
|
|
210
|
+
if (this._isAdminOfParent(parentId, ref.type)) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
override isAuthor(options?: { silent?: boolean }): boolean {
|
|
218
|
+
const silent = options?.silent ?? true;
|
|
219
|
+
try {
|
|
220
|
+
// Note : on utilise `isConnected` (user logged in) au lieu de `isMe`
|
|
221
|
+
// (l'entité IS le user logged in) qui est sémantiquement faux sur Answer.
|
|
222
|
+
// `_checkAccess` du parent est private et utilise isMe — non réutilisable ici.
|
|
223
|
+
if (!this.isConnected) throw new ApiError("Vous devez être connecté pour vérifier l'auteur.", 401);
|
|
224
|
+
if (!this.id) throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
225
|
+
if (!this.serverData) throw new ApiError("Aucune donnée serveur disponible.", 404);
|
|
226
|
+
if (!this.userId) throw new ApiError("Utilisateur non connecté.", 401);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
if (silent) return false;
|
|
229
|
+
throw e;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const user = this._serverData.user;
|
|
233
|
+
if (!user) return false;
|
|
234
|
+
|
|
235
|
+
const authorId = typeof user === "string" ? user : (user as { id?: string }).id;
|
|
236
|
+
return Boolean(
|
|
237
|
+
this.userId
|
|
238
|
+
&& typeof authorId === "string"
|
|
239
|
+
&& authorId === this.userId
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
170
243
|
/**
|
|
171
244
|
* Transforme les champs imbriqués (user, context etc.) en instances d'entités.
|
|
172
245
|
* @param data - Les données brutes du serveur.
|
|
@@ -269,12 +342,26 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
269
342
|
* - `serverData` et `draftData` sont vidés (sans casser la réactivité)
|
|
270
343
|
* - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
|
|
271
344
|
*
|
|
345
|
+
* **Guard** : autorisation côté client si l'utilisateur courant est :
|
|
346
|
+
* - l'**auteur** de l'Answer (`serverData.user === userId`), OU
|
|
347
|
+
* - **admin** d'un parent du Form (org/project) via la hiérarchie
|
|
348
|
+
* (`form.serverData.parent[orgId]` × `userContext.links.memberOf[orgId].isAdmin`).
|
|
349
|
+
*
|
|
350
|
+
* Le check utilise `isAuthorOrAdmin({checkHierarchy: true})` qui s'appuie sur
|
|
351
|
+
* l'override `Answer._isAdminViaHierarchy` (remonte via parent instance Form).
|
|
352
|
+
*
|
|
353
|
+
* **Pré-requis** :
|
|
354
|
+
* - `this.parent` doit être un Form (cas `form.answer({id})`)
|
|
355
|
+
* - `userContext` (User connecté) doit avoir `serverData.links` chargés (cas `api.me()`)
|
|
356
|
+
* Sans ces pré-requis, seul `isAuthor` peut autoriser.
|
|
357
|
+
*
|
|
272
358
|
* @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
|
|
273
359
|
* @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
|
|
360
|
+
* @throws {ApiError} 403 si ni auteur ni admin du parent.
|
|
274
361
|
*
|
|
275
362
|
* @example
|
|
276
363
|
* const answer = await form.answer({ id: "..." });
|
|
277
|
-
* await answer.delete();
|
|
364
|
+
* await answer.delete(); // OK si auteur OU admin org/project parent du Form
|
|
278
365
|
* answer._isDeleted; // true
|
|
279
366
|
*/
|
|
280
367
|
async delete(reason: string = "delete coform answer"): Promise<void> {
|
|
@@ -282,6 +369,15 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
282
369
|
throw new ApiError("Vous devez fournir un id pour supprimer une Answer.", 400);
|
|
283
370
|
}
|
|
284
371
|
|
|
372
|
+
// Guard : auteur OU admin du parent Form (org/project).
|
|
373
|
+
if (!this.isAuthorOrAdmin({ checkHierarchy: true })) {
|
|
374
|
+
throw new ApiError(
|
|
375
|
+
"Suppression refusée — vous devez être l'auteur de l'Answer "
|
|
376
|
+
+ "ou administrateur du parent (organisation/projet) du Form.",
|
|
377
|
+
403
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
285
381
|
const data: DeleteElementData = {
|
|
286
382
|
reason,
|
|
287
383
|
pathParams: { type: "answers", id: this.id },
|
|
@@ -296,6 +392,54 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
296
392
|
this._isDeleted = true;
|
|
297
393
|
}
|
|
298
394
|
|
|
395
|
+
/**
|
|
396
|
+
* {@inheritDoc BaseEntity#deleteFile}
|
|
397
|
+
*
|
|
398
|
+
* Override avec **cleanup local** : après suppression backend, retire le
|
|
399
|
+
* `docId` des structures uploader normalisées dans `serverData.answers` et
|
|
400
|
+
* `_draftData.answers` (format `{updateDate, files: {docId: docPath}}`
|
|
401
|
+
* produit par `processUploads()` et `_transformServerData`).
|
|
402
|
+
*
|
|
403
|
+
* Le caller n'a pas besoin de refetch — `answer.serverData.answers[*][*].files`
|
|
404
|
+
* reflète directement l'état post-suppression.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
408
|
+
* await answer.deleteFile(files[0].docId);
|
|
409
|
+
* // answer.serverData.answers[sf][input].files[files[0].docId] === undefined
|
|
410
|
+
*/
|
|
411
|
+
override async deleteFile(docId: string): Promise<void> {
|
|
412
|
+
await super.deleteFile(docId);
|
|
413
|
+
|
|
414
|
+
// Cleanup local : retire le docId des structures uploader normalisées
|
|
415
|
+
this._removeDocIdFromUploaderFields(this._serverData.answers, docId);
|
|
416
|
+
this._removeDocIdFromUploaderFields(this._draftData.answers as Record<string, unknown> | undefined, docId);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Walk `answers[subFormId][inputId]` et retire `docId` des structures
|
|
421
|
+
* `{updateDate, files: {docId: docPath}}` (format canonique uploader).
|
|
422
|
+
* Mutation in-place pour préserver la réactivité.
|
|
423
|
+
* @private
|
|
424
|
+
*/
|
|
425
|
+
private _removeDocIdFromUploaderFields(
|
|
426
|
+
answers: Record<string, unknown> | undefined,
|
|
427
|
+
docId: string,
|
|
428
|
+
): void {
|
|
429
|
+
if (!answers || typeof answers !== "object") return;
|
|
430
|
+
|
|
431
|
+
for (const subFormData of Object.values(answers)) {
|
|
432
|
+
if (!this._isObjectRecord(subFormData)) continue;
|
|
433
|
+
for (const inputValue of Object.values(subFormData)) {
|
|
434
|
+
if (!this._isObjectRecord(inputValue)) continue;
|
|
435
|
+
const files = (inputValue as { files?: unknown }).files;
|
|
436
|
+
if (this._isObjectRecord(files) && docId in files) {
|
|
437
|
+
delete (files as Record<string, unknown>)[docId];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
299
443
|
/**
|
|
300
444
|
* Récupère la liste des fichiers attachés à un input précis de cette Answer
|
|
301
445
|
* via `COFORM_GET_ANSWER_FILES`.
|
|
@@ -376,6 +520,13 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
376
520
|
/**
|
|
377
521
|
* Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
|
|
378
522
|
*
|
|
523
|
+
* **Building block du pipeline** : conçu pour être utilisé via
|
|
524
|
+
* `processUploads()` suivi de `save()`. L'Answer créée par le premier upload
|
|
525
|
+
* est en état "placeholder" côté backend (champ `user` non posé) — `save()`
|
|
526
|
+
* finalise. Appeler `get()` ou `delete()` (via l'entité) sur une Answer
|
|
527
|
+
* upload-only sans save échouera : le backend la considère comme orpheline
|
|
528
|
+
* (`editDeniedReason: "not_owner"`, stub sans `data.id`).
|
|
529
|
+
*
|
|
379
530
|
* **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
|
|
380
531
|
* renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
|
|
381
532
|
* — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
|
package/src/api/BaseEntity.ts
CHANGED
|
@@ -58,7 +58,8 @@ import type {
|
|
|
58
58
|
LinkMediawikiAccountData,
|
|
59
59
|
UnlinkMediawikiAccountData,
|
|
60
60
|
GetMediawikiContributionsData,
|
|
61
|
-
CoremuOperationData
|
|
61
|
+
CoremuOperationData,
|
|
62
|
+
DeleteDocumentByIdData
|
|
62
63
|
} from "./EndpointApi.types.js";
|
|
63
64
|
import type { GetElementsKeyResponse } from "../types/api-responses.js";
|
|
64
65
|
import type { TransformsMap } from "../types/entities.js";
|
|
@@ -1225,6 +1226,39 @@ export class BaseEntity<TServerData = any> {
|
|
|
1225
1226
|
throw new ApiError("Type de fichier non reconnu pour l'upload.", 400);
|
|
1226
1227
|
}
|
|
1227
1228
|
|
|
1229
|
+
/**
|
|
1230
|
+
* Supprime un document (fichier ou image) par son `docId` MongoDB via
|
|
1231
|
+
* `DELETE_DOCUMENT_BY_ID`.
|
|
1232
|
+
*
|
|
1233
|
+
* Méthode générique exposée sur toutes les entités — le endpoint backend
|
|
1234
|
+
* opère par docId seul, sans contexte parent requis. Disponible ici pour
|
|
1235
|
+
* être découvrable et **override-able** par les entités qui veulent un
|
|
1236
|
+
* cleanup local après suppression (ex: `News.deleteFile` pourrait retirer
|
|
1237
|
+
* l'id de `mediaImg.images` côté serverData).
|
|
1238
|
+
*
|
|
1239
|
+
* Ne met PAS à jour `serverData` après suppression — le caller doit
|
|
1240
|
+
* invalider son cache local s'il en a un (cas typique : React Query).
|
|
1241
|
+
*
|
|
1242
|
+
* @param docId - ID MongoDB du document (24 hex)
|
|
1243
|
+
* @throws {ApiError} 400 si `docId` invalide.
|
|
1244
|
+
*
|
|
1245
|
+
* @example
|
|
1246
|
+
* // Depuis une Answer (suppression d'un fichier uploader)
|
|
1247
|
+
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
1248
|
+
* await answer.deleteFile(files[0].docId);
|
|
1249
|
+
*
|
|
1250
|
+
* @example
|
|
1251
|
+
* // Depuis n'importe quelle entité qui possède un docId
|
|
1252
|
+
* await news.deleteFile(imageDocId);
|
|
1253
|
+
*/
|
|
1254
|
+
async deleteFile(docId: string): Promise<void> {
|
|
1255
|
+
if (typeof docId !== "string" || docId.length !== 24) {
|
|
1256
|
+
throw new ApiError("docId requis (24 hex) pour supprimer un document.", 400);
|
|
1257
|
+
}
|
|
1258
|
+
const payload: DeleteDocumentByIdData = { pathParams: { id: docId } };
|
|
1259
|
+
await this.callIsConnected(() => this.endpointApi.deleteDocumentById(payload));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1228
1262
|
/**
|
|
1229
1263
|
* Valide les entrées d'upload de fichiers.
|
|
1230
1264
|
*
|
|
@@ -2505,10 +2539,15 @@ export class BaseEntity<TServerData = any> {
|
|
|
2505
2539
|
}
|
|
2506
2540
|
|
|
2507
2541
|
/**
|
|
2508
|
-
* Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste)
|
|
2509
|
-
*
|
|
2542
|
+
* Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste).
|
|
2543
|
+
*
|
|
2544
|
+
* Note : passé `protected` pour permettre l'override par les entités dont la
|
|
2545
|
+
* hiérarchie n'est pas un simple champ `serverData.parent` ou `.organizer`
|
|
2546
|
+
* (cf. `Answer._isAdminViaHierarchy` qui remonte via son parent instance Form).
|
|
2547
|
+
*
|
|
2548
|
+
* @protected
|
|
2510
2549
|
*/
|
|
2511
|
-
|
|
2550
|
+
protected _isAdminViaHierarchy(): boolean {
|
|
2512
2551
|
const entityType = this.getEntityType();
|
|
2513
2552
|
const entityData = this.serverData as any;
|
|
2514
2553
|
|
|
@@ -2545,10 +2584,14 @@ export class BaseEntity<TServerData = any> {
|
|
|
2545
2584
|
}
|
|
2546
2585
|
|
|
2547
2586
|
/**
|
|
2548
|
-
* Vérifie si l'utilisateur est admin d'un parent spécifique
|
|
2549
|
-
*
|
|
2587
|
+
* Vérifie si l'utilisateur est admin d'un parent spécifique.
|
|
2588
|
+
*
|
|
2589
|
+
* Note : passé `protected` pour être utilisable par les overrides de
|
|
2590
|
+
* `_isAdminViaHierarchy()` (cf. `Answer._isAdminViaHierarchy`).
|
|
2591
|
+
*
|
|
2592
|
+
* @protected
|
|
2550
2593
|
*/
|
|
2551
|
-
|
|
2594
|
+
protected _isAdminOfParent(parentId: string, parentType: string): boolean {
|
|
2552
2595
|
const userLinks = this.userContext?.serverData?.links;
|
|
2553
2596
|
if (!userLinks) return false;
|
|
2554
2597
|
|
|
@@ -4152,10 +4195,13 @@ export class BaseEntity<TServerData = any> {
|
|
|
4152
4195
|
*
|
|
4153
4196
|
* @param options - Options de vérification.
|
|
4154
4197
|
* @param options.silent - Si `true`, retourne `false` au lieu de lever une exception. Par défaut `true`.
|
|
4198
|
+
* @param options.checkHierarchy - Si `true`, propagé à `isAdmin` pour vérifier
|
|
4199
|
+
* également la hiérarchie parent (ex: `Answer.isAdmin({checkHierarchy: true})`
|
|
4200
|
+
* remonte vers org/project du Form parent).
|
|
4155
4201
|
* @returns - `true` si l'utilisateur est l'auteur ou administrateur, `false` sinon.
|
|
4156
4202
|
* @throws {ApiError} - Si `silent` est `false` et que les préconditions ne sont pas remplies.
|
|
4157
4203
|
*/
|
|
4158
|
-
isAuthorOrAdmin(options?: { silent?: boolean }): boolean {
|
|
4204
|
+
isAuthorOrAdmin(options?: { silent?: boolean; checkHierarchy?: boolean }): boolean {
|
|
4159
4205
|
return this.isAuthor() || this.isAdmin(options);
|
|
4160
4206
|
}
|
|
4161
4207
|
|
package/types/api/Answer.d.ts
CHANGED
|
@@ -136,6 +136,34 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
136
136
|
* Les autres sont calculés/posés par le backend.
|
|
137
137
|
*/
|
|
138
138
|
removeFields: string[];
|
|
139
|
+
/**
|
|
140
|
+
* {@inheritDoc BaseEntity#isAuthor}
|
|
141
|
+
*
|
|
142
|
+
* Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
|
|
143
|
+
* comme la version par défaut de BaseEntity). Le `user` peut être :
|
|
144
|
+
* - un `string` (ID MongoDB brut renvoyé par le backend)
|
|
145
|
+
* - une instance `User` (après `_transformServerData` qui le link en entité)
|
|
146
|
+
*
|
|
147
|
+
* Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
|
|
148
|
+
* la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
|
|
149
|
+
*/
|
|
150
|
+
/**
|
|
151
|
+
* {@inheritDoc BaseEntity#_isAdminViaHierarchy}
|
|
152
|
+
*
|
|
153
|
+
* Override Answer : la hiérarchie d'Answer passe par son **parent instance
|
|
154
|
+
* Form** (pas par `serverData.parent` direct comme pour `events`/`projects`/`poi`).
|
|
155
|
+
*
|
|
156
|
+
* Chaîne : `Answer → parent (Form) → form.serverData.parent ({[orgId]: {type, name}}) → check userContext.links`.
|
|
157
|
+
*
|
|
158
|
+
* Si le parent n'est pas un Form (cas `api.answer({id})` direct sans contexte),
|
|
159
|
+
* renvoie `false` — impossible de vérifier la hiérarchie sans le Form.
|
|
160
|
+
*
|
|
161
|
+
* @protected
|
|
162
|
+
*/
|
|
163
|
+
protected _isAdminViaHierarchy(): boolean;
|
|
164
|
+
isAuthor(options?: {
|
|
165
|
+
silent?: boolean;
|
|
166
|
+
}): boolean;
|
|
139
167
|
/**
|
|
140
168
|
* Transforme les champs imbriqués (user, context etc.) en instances d'entités.
|
|
141
169
|
* @param data - Les données brutes du serveur.
|
|
@@ -183,15 +211,53 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
183
211
|
* - `serverData` et `draftData` sont vidés (sans casser la réactivité)
|
|
184
212
|
* - `_isDeleted = true` → toute mutation ultérieure (`save`, `get`, etc.) throw
|
|
185
213
|
*
|
|
214
|
+
* **Guard** : autorisation côté client si l'utilisateur courant est :
|
|
215
|
+
* - l'**auteur** de l'Answer (`serverData.user === userId`), OU
|
|
216
|
+
* - **admin** d'un parent du Form (org/project) via la hiérarchie
|
|
217
|
+
* (`form.serverData.parent[orgId]` × `userContext.links.memberOf[orgId].isAdmin`).
|
|
218
|
+
*
|
|
219
|
+
* Le check utilise `isAuthorOrAdmin({checkHierarchy: true})` qui s'appuie sur
|
|
220
|
+
* l'override `Answer._isAdminViaHierarchy` (remonte via parent instance Form).
|
|
221
|
+
*
|
|
222
|
+
* **Pré-requis** :
|
|
223
|
+
* - `this.parent` doit être un Form (cas `form.answer({id})`)
|
|
224
|
+
* - `userContext` (User connecté) doit avoir `serverData.links` chargés (cas `api.me()`)
|
|
225
|
+
* Sans ces pré-requis, seul `isAuthor` peut autoriser.
|
|
226
|
+
*
|
|
186
227
|
* @param reason - Raison libre transmise au backend (audit). Défaut : `"delete coform answer"`.
|
|
187
228
|
* @throws {ApiError} 400 si l'Answer n'a pas d'id (jamais sauvegardée).
|
|
229
|
+
* @throws {ApiError} 403 si ni auteur ni admin du parent.
|
|
188
230
|
*
|
|
189
231
|
* @example
|
|
190
232
|
* const answer = await form.answer({ id: "..." });
|
|
191
|
-
* await answer.delete();
|
|
233
|
+
* await answer.delete(); // OK si auteur OU admin org/project parent du Form
|
|
192
234
|
* answer._isDeleted; // true
|
|
193
235
|
*/
|
|
194
236
|
delete(reason?: string): Promise<void>;
|
|
237
|
+
/**
|
|
238
|
+
* {@inheritDoc BaseEntity#deleteFile}
|
|
239
|
+
*
|
|
240
|
+
* Override avec **cleanup local** : après suppression backend, retire le
|
|
241
|
+
* `docId` des structures uploader normalisées dans `serverData.answers` et
|
|
242
|
+
* `_draftData.answers` (format `{updateDate, files: {docId: docPath}}`
|
|
243
|
+
* produit par `processUploads()` et `_transformServerData`).
|
|
244
|
+
*
|
|
245
|
+
* Le caller n'a pas besoin de refetch — `answer.serverData.answers[*][*].files`
|
|
246
|
+
* reflète directement l'état post-suppression.
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
250
|
+
* await answer.deleteFile(files[0].docId);
|
|
251
|
+
* // answer.serverData.answers[sf][input].files[files[0].docId] === undefined
|
|
252
|
+
*/
|
|
253
|
+
deleteFile(docId: string): Promise<void>;
|
|
254
|
+
/**
|
|
255
|
+
* Walk `answers[subFormId][inputId]` et retire `docId` des structures
|
|
256
|
+
* `{updateDate, files: {docId: docPath}}` (format canonique uploader).
|
|
257
|
+
* Mutation in-place pour préserver la réactivité.
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
private _removeDocIdFromUploaderFields;
|
|
195
261
|
/**
|
|
196
262
|
* Récupère la liste des fichiers attachés à un input précis de cette Answer
|
|
197
263
|
* via `COFORM_GET_ANSWER_FILES`.
|
|
@@ -223,6 +289,13 @@ export declare class Answer extends BaseEntity<AnswerItemNormalized> {
|
|
|
223
289
|
/**
|
|
224
290
|
* Upload un fichier rattaché à cette Answer via `COFORM_UPLOAD_ANSWER_FILE`.
|
|
225
291
|
*
|
|
292
|
+
* **Building block du pipeline** : conçu pour être utilisé via
|
|
293
|
+
* `processUploads()` suivi de `save()`. L'Answer créée par le premier upload
|
|
294
|
+
* est en état "placeholder" côté backend (champ `user` non posé) — `save()`
|
|
295
|
+
* finalise. Appeler `get()` ou `delete()` (via l'entité) sur une Answer
|
|
296
|
+
* upload-only sans save échouera : le backend la considère comme orpheline
|
|
297
|
+
* (`editDeniedReason: "not_owner"`, stub sans `data.id`).
|
|
298
|
+
*
|
|
226
299
|
* **Cas premier upload (Answer sans id)** : le backend crée l'Answer et
|
|
227
300
|
* renvoie `answerId`. La méthode pose alors `this._draftData.id = answerId`
|
|
228
301
|
* — les uploads suivants utiliseront cet id, et un `save()` ultérieur passera
|
|
@@ -562,6 +562,32 @@ export declare class BaseEntity<TServerData = any> {
|
|
|
562
562
|
qqfilename: string;
|
|
563
563
|
qqtotalfilesize: number;
|
|
564
564
|
}>;
|
|
565
|
+
/**
|
|
566
|
+
* Supprime un document (fichier ou image) par son `docId` MongoDB via
|
|
567
|
+
* `DELETE_DOCUMENT_BY_ID`.
|
|
568
|
+
*
|
|
569
|
+
* Méthode générique exposée sur toutes les entités — le endpoint backend
|
|
570
|
+
* opère par docId seul, sans contexte parent requis. Disponible ici pour
|
|
571
|
+
* être découvrable et **override-able** par les entités qui veulent un
|
|
572
|
+
* cleanup local après suppression (ex: `News.deleteFile` pourrait retirer
|
|
573
|
+
* l'id de `mediaImg.images` côté serverData).
|
|
574
|
+
*
|
|
575
|
+
* Ne met PAS à jour `serverData` après suppression — le caller doit
|
|
576
|
+
* invalider son cache local s'il en a un (cas typique : React Query).
|
|
577
|
+
*
|
|
578
|
+
* @param docId - ID MongoDB du document (24 hex)
|
|
579
|
+
* @throws {ApiError} 400 si `docId` invalide.
|
|
580
|
+
*
|
|
581
|
+
* @example
|
|
582
|
+
* // Depuis une Answer (suppression d'un fichier uploader)
|
|
583
|
+
* const files = await answer.getFiles({ subKey: "sf.input", docType: "image" });
|
|
584
|
+
* await answer.deleteFile(files[0].docId);
|
|
585
|
+
*
|
|
586
|
+
* @example
|
|
587
|
+
* // Depuis n'importe quelle entité qui possède un docId
|
|
588
|
+
* await news.deleteFile(imageDocId);
|
|
589
|
+
*/
|
|
590
|
+
deleteFile(docId: string): Promise<void>;
|
|
565
591
|
/**
|
|
566
592
|
* Valide les entrées d'upload de fichiers.
|
|
567
593
|
*
|
|
@@ -938,15 +964,24 @@ export declare class BaseEntity<TServerData = any> {
|
|
|
938
964
|
*/
|
|
939
965
|
private _extractUserContext;
|
|
940
966
|
/**
|
|
941
|
-
* Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste)
|
|
942
|
-
*
|
|
967
|
+
* Vérifie si l'utilisateur est admin via la hiérarchie parent (logique généraliste).
|
|
968
|
+
*
|
|
969
|
+
* Note : passé `protected` pour permettre l'override par les entités dont la
|
|
970
|
+
* hiérarchie n'est pas un simple champ `serverData.parent` ou `.organizer`
|
|
971
|
+
* (cf. `Answer._isAdminViaHierarchy` qui remonte via son parent instance Form).
|
|
972
|
+
*
|
|
973
|
+
* @protected
|
|
943
974
|
*/
|
|
944
|
-
|
|
975
|
+
protected _isAdminViaHierarchy(): boolean;
|
|
945
976
|
/**
|
|
946
|
-
* Vérifie si l'utilisateur est admin d'un parent spécifique
|
|
947
|
-
*
|
|
977
|
+
* Vérifie si l'utilisateur est admin d'un parent spécifique.
|
|
978
|
+
*
|
|
979
|
+
* Note : passé `protected` pour être utilisable par les overrides de
|
|
980
|
+
* `_isAdminViaHierarchy()` (cf. `Answer._isAdminViaHierarchy`).
|
|
981
|
+
*
|
|
982
|
+
* @protected
|
|
948
983
|
*/
|
|
949
|
-
|
|
984
|
+
protected _isAdminOfParent(parentId: string, parentType: string): boolean;
|
|
950
985
|
/**
|
|
951
986
|
* Construit dynamiquement des filtres sur un lien entre entités
|
|
952
987
|
*
|
|
@@ -1503,11 +1538,15 @@ export declare class BaseEntity<TServerData = any> {
|
|
|
1503
1538
|
*
|
|
1504
1539
|
* @param options - Options de vérification.
|
|
1505
1540
|
* @param options.silent - Si `true`, retourne `false` au lieu de lever une exception. Par défaut `true`.
|
|
1541
|
+
* @param options.checkHierarchy - Si `true`, propagé à `isAdmin` pour vérifier
|
|
1542
|
+
* également la hiérarchie parent (ex: `Answer.isAdmin({checkHierarchy: true})`
|
|
1543
|
+
* remonte vers org/project du Form parent).
|
|
1506
1544
|
* @returns - `true` si l'utilisateur est l'auteur ou administrateur, `false` sinon.
|
|
1507
1545
|
* @throws {ApiError} - Si `silent` est `false` et que les préconditions ne sont pas remplies.
|
|
1508
1546
|
*/
|
|
1509
1547
|
isAuthorOrAdmin(options?: {
|
|
1510
1548
|
silent?: boolean;
|
|
1549
|
+
checkHierarchy?: boolean;
|
|
1511
1550
|
}): boolean;
|
|
1512
1551
|
/**
|
|
1513
1552
|
* Vérifie si l'utilisateur est membre de l'entité.
|