@communecter/cocolight-api-client 1.0.135 → 1.0.136

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@communecter/cocolight-api-client",
3
- "version": "1.0.135",
3
+ "version": "1.0.136",
4
4
  "description": "Client Axios simplifié pour l'API cocolight",
5
5
  "repository": {
6
6
  "type": "git",
package/src/Api.ts CHANGED
@@ -16,6 +16,7 @@ import { User } from "./api/User.js";
16
16
  import { UserApi } from "./api/UserApi.js";
17
17
  import { ApiAuthenticationError, ApiClientError, ApiError, ApiResponseError } from "./error.js";
18
18
 
19
+ import type { SearchTagsData } from "./api/EndpointApi.types.js";
19
20
  import type ApiClient from "./ApiClient.js";
20
21
  import type { ApiClientOptions } from "./ApiClient.js";
21
22
  import type { GetElementsKeyResponse } from "./types/api-responses.js";
@@ -310,6 +311,31 @@ export default class Api {
310
311
  return new EndpointApi(this._client);
311
312
  }
312
313
 
314
+ /**
315
+ * Recherche des tags correspondant à un mot-clé.
316
+ *
317
+ * Wrap de `SEARCH_TAGS` (auth=none) — accessible même non connecté.
318
+ * Méthode exposée sur la facade `Api` (pas d'entité hôte naturelle pour
319
+ * un index global de tags).
320
+ *
321
+ * @param q - Mot-clé de recherche (au moins 1 caractère).
322
+ * @returns Tableau de résultats : soit `[{tag}]` si aucun match (echo du
323
+ * terme), soit `[{_id, tag, field_length}, ...]` avec les tags
324
+ * existants triés par `field_length`.
325
+ * @throws {ApiError} 400 si `q` vide.
326
+ *
327
+ * @example
328
+ * const results = await api.searchTags("permac");
329
+ * results.forEach(t => console.log(t.tag, t.field_length));
330
+ */
331
+ async searchTags(q: string): Promise<unknown> {
332
+ if (typeof q !== "string" || q.length === 0) {
333
+ throw new ApiError("Le mot-clé `q` est requis pour searchTags.", 400);
334
+ }
335
+ const payload: SearchTagsData = { pathParams: { q } };
336
+ return this.endpointApi.searchTags(payload);
337
+ }
338
+
313
339
  /**
314
340
  * Déconnecte l'utilisateur et réinitialise la session.
315
341
  */
package/src/api/Action.ts CHANGED
@@ -557,16 +557,12 @@ export class Action extends BaseEntity<ActionItemNormalized> {
557
557
  // ───────────────────────────────────────────────────────────────────────────
558
558
 
559
559
  /**
560
- * Helper privé : envoie un UPDATE_PATH_VALUE pour un path donné.
560
+ * Helper privé : délègue à `BaseEntity.updateField()` pour un path donné.
561
+ * Bénéficie du normalize automatique (R0 Date → isoDate, R1 collapse setType,
562
+ * R2 pull + empty → "", R6 drop setType vide).
561
563
  */
562
564
  private async _updatePath(path: string, value: unknown, setType?: string): Promise<unknown> {
563
- return this.endpointApi.updatePathValue({
564
- id: this.id!,
565
- collection: "actions",
566
- path,
567
- value: value as { [k: string]: unknown },
568
- ...(setType ? { setType } : {})
569
- });
565
+ return this.updateField(path, value, setType ? { setType } : {});
570
566
  }
571
567
 
572
568
  /**
package/src/api/Answer.ts CHANGED
@@ -167,17 +167,6 @@ 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
170
  /**
182
171
  * {@inheritDoc BaseEntity#_isAdminViaHierarchy}
183
172
  *
@@ -214,6 +203,17 @@ export class Answer extends BaseEntity<AnswerItemNormalized> {
214
203
  return false;
215
204
  }
216
205
 
206
+ /**
207
+ * {@inheritDoc BaseEntity#isAuthor}
208
+ *
209
+ * Override Answer : le champ d'autorité est `serverData.user` (pas `creator`
210
+ * comme la version par défaut de BaseEntity). Le `user` peut être :
211
+ * - un `string` (ID MongoDB brut renvoyé par le backend)
212
+ * - une instance `User` (après `_transformServerData` qui le link en entité)
213
+ *
214
+ * Les pré-conditions (connexion, id, serverData) sont vérifiées comme dans
215
+ * la version de base — `silent: true` (défaut) renvoie `false` au lieu de throw.
216
+ */
217
217
  override isAuthor(options?: { silent?: boolean }): boolean {
218
218
  const silent = options?.silent ?? true;
219
219
  try {
@@ -59,7 +59,8 @@ import type {
59
59
  UnlinkMediawikiAccountData,
60
60
  GetMediawikiContributionsData,
61
61
  CoremuOperationData,
62
- DeleteDocumentByIdData
62
+ DeleteDocumentByIdData,
63
+ UpdatePathValueData
63
64
  } from "./EndpointApi.types.js";
64
65
  import type { GetElementsKeyResponse } from "../types/api-responses.js";
65
66
  import type { TransformsMap } from "../types/entities.js";
@@ -119,6 +120,52 @@ type ParentLike = BaseEntity<any> & { apiClient: ApiClient, userContext?: User |
119
120
  */
120
121
  export type SearchCostumVariant = "default" | "navigator-tl";
121
122
 
123
+ /**
124
+ * Types de coercion backend supportés par `UPDATE_PATH_VALUE.setType`.
125
+ *
126
+ * Référence côté backend : `string_set_type` (PHP) accepte ces 5 valeurs +
127
+ * tout autre string (passthrough — pas de coercion). Le `& Record<never, never>`
128
+ * garde l'autocomplete des literals tout en acceptant des strings custom.
129
+ *
130
+ * - `"int"` → `intval()` (PHP)
131
+ * - `"float"` → `floatval()`
132
+ * - `"boolean"` / `"bool"` → `FILTER_VALIDATE_BOOLEAN` (alias)
133
+ * - `"isoDate"` → parse multi-format → `MongoDate`
134
+ * Formats acceptés : ISO 8601 natif, `m-d-Y`, `d-m-Y`, `{sec, usec}` legacy,
135
+ * string `"now"` (date actuelle), slashes auto-convertis en tirets.
136
+ * - `"timestamp"` → parse → Unix seconds (`int`)
137
+ */
138
+ export type SetTypeValue =
139
+ | "int"
140
+ | "float"
141
+ | "boolean"
142
+ | "bool"
143
+ | "isoDate"
144
+ | "timestamp"
145
+ | (string & Record<never, never>);
146
+
147
+ /**
148
+ * Magic values reconnues par `UPDATE_PATH_VALUE` côté backend.
149
+ * Utilisable comme `value` ou `path` selon le cas.
150
+ */
151
+ export const UPDATE_PATH_MAGIC = {
152
+ /**
153
+ * `value: "updatedTime"` → backend remplace par `time()` Unix seconds courant
154
+ * (utile pour `lastEditedAt`, audit trail, etc. sans avoir à fournir un timestamp côté client).
155
+ */
156
+ CURRENT_TIME: "updatedTime",
157
+ /**
158
+ * `value: "now"` avec `setType: "isoDate"` → résolu côté backend en `MongoDate(now)`.
159
+ */
160
+ NOW: "now",
161
+ /**
162
+ * `path: "allToRoot"` → la `value` (qui doit être un objet) est exposée à la
163
+ * racine de l'entité : chaque clé devient un champ racine. Combiner avec
164
+ * `updatePartial: true` pour un merge avec préservation des autres champs.
165
+ */
166
+ PATH_ALL_TO_ROOT: "allToRoot",
167
+ } as const;
168
+
122
169
  const SEARCH_COSTUM_ENDPOINTS = {
123
170
  "default": "globalAutocompleteCostum",
124
171
  "navigator-tl": "navigatorGettl",
@@ -1259,6 +1306,181 @@ export class BaseEntity<TServerData = any> {
1259
1306
  await this.callIsConnected(() => this.endpointApi.deleteDocumentById(payload));
1260
1307
  }
1261
1308
 
1309
+ /**
1310
+ * Met à jour un champ unique (ou multi-fields via `updatePartial`) sur cette
1311
+ * entité via `UPDATE_PATH_VALUE`.
1312
+ *
1313
+ * **Cas d'usage couverts** :
1314
+ * - `$set` simple : `entity.updateField("name", "Nouveau nom")`
1315
+ * - `$unset` : `entity.updateField("description", null)`
1316
+ * - `$push` array : `entity.updateField("tags", "x", { arrayForm: true })`
1317
+ * - `$pull` array : `entity.updateField("tags.3", null, { pull: "tags" })`
1318
+ * - `$pullAll` array : `entity.updateField("tags", ["a","b"], { arrayForm: true, pull: "tags" })`
1319
+ * - Coercion type : `entity.updateField("credits", 100, { setType: "int" })`
1320
+ * - Date auto : `entity.updateField("startDate", new Date())` → setType "isoDate" + ISO string
1321
+ * - Multi-field merge : `entity.updateField("address", {...}, { updatePartial: true })`
1322
+ * - Auth sub-form : `entity.updateField("...", value, { formParentId: "..." })`
1323
+ *
1324
+ * **Magic values** :
1325
+ * - `value: "updatedTime"` → backend remplace par `time()` Unix seconds
1326
+ * - `value: "now"` + `setType: "isoDate"` → résout en date actuelle
1327
+ * - `path: "allToRoot"` → value exposée à la racine de l'entité
1328
+ *
1329
+ * Le payload est normalisé via `_normalizeUpdatePathPayload` avant envoi
1330
+ * (cf. les 4 règles documentées sur cette méthode).
1331
+ *
1332
+ * @param path - Chemin MongoDB dot-notation (ex: `"address.street"`, `"answers.aapStep1.depense.3"`).
1333
+ * @param value - Valeur à poser. Accepte `Date` (auto-convertie en ISO + setType isoDate).
1334
+ * @param opts - Options : `setType`, `arrayForm`, `pull`, `updatePartial`, `formParentId`, `edit`.
1335
+ * @returns Réponse brute de l'API.
1336
+ * @throws {ApiError} 404 si l'entité n'a pas d'id.
1337
+ * @throws {ApiError} 400 si `value === undefined`, `path` vide, ou conflit `arrayForm` + `pull` sans value array, ou `updatePartial` sans value object.
1338
+ *
1339
+ * @example
1340
+ * // Update simple
1341
+ * await org.updateField("name", "Nouvelle organisation");
1342
+ *
1343
+ * @example
1344
+ * // Date instance auto-convertie
1345
+ * await action.updateField("startDate", new Date());
1346
+ *
1347
+ * @example
1348
+ * // Multi-field merge (préserve les autres clés de address)
1349
+ * await org.updateField("address", {
1350
+ * street: "123 rue X",
1351
+ * city: "Paris",
1352
+ * }, { updatePartial: true });
1353
+ *
1354
+ * @example
1355
+ * // Push dans array
1356
+ * await project.updateField("tags", "permaculture", { arrayForm: true });
1357
+ *
1358
+ * @example
1359
+ * // Pull from array (avec normalisation R2 : null → "")
1360
+ * await project.updateField("oceco.milestones.3", null, { pull: "oceco.milestones" });
1361
+ */
1362
+ async updateField(
1363
+ path: string,
1364
+ value: unknown,
1365
+ opts: {
1366
+ setType?: SetTypeValue | Array<{ path: string; type: SetTypeValue }>;
1367
+ arrayForm?: boolean;
1368
+ pull?: string;
1369
+ updatePartial?: boolean;
1370
+ formParentId?: string;
1371
+ edit?: boolean;
1372
+ } = {},
1373
+ ): Promise<unknown> {
1374
+ // R3 — value: undefined → throw (distinction nette vs null pour $unset)
1375
+ if (value === undefined) {
1376
+ throw new ApiError(
1377
+ "`value` ne peut pas être undefined. Utiliser `null` pour $unset.",
1378
+ 400
1379
+ );
1380
+ }
1381
+ if (!this.id) {
1382
+ throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
1383
+ }
1384
+ // R4 — path validation
1385
+ if (typeof path !== "string" || path.length === 0) {
1386
+ throw new ApiError("`path` est requis et doit être une string non vide.", 400);
1387
+ }
1388
+ // R5 — arrayForm + pull : autorisé uniquement si value est array ($pullAll)
1389
+ if (opts.arrayForm && opts.pull) {
1390
+ if (!Array.isArray(value)) {
1391
+ throw new ApiError(
1392
+ "`arrayForm` + `pull` requièrent value: array (mode $pullAll). "
1393
+ + "Pour un $pull simple, utiliser uniquement `pull` (sans arrayForm).",
1394
+ 400
1395
+ );
1396
+ }
1397
+ }
1398
+ // R9 — updatePartial requiert value: object
1399
+ if (opts.updatePartial && (typeof value !== "object" || value === null || Array.isArray(value))) {
1400
+ throw new ApiError(
1401
+ "`updatePartial: true` requiert value: object (les sous-clés deviennent des sub-paths).",
1402
+ 400
1403
+ );
1404
+ }
1405
+ // Vérif collection : UPDATE_PATH_VALUE backend exclut badges/news/comments/classifieds
1406
+ // (ces entités ont leurs propres endpoints update — cf. enum schema).
1407
+ this._assertNotEntityType("updateField", ["badges", "news", "comments", "classifieds"]);
1408
+
1409
+ // Construction du payload
1410
+ const payload: UpdatePathValueData = {
1411
+ id: this.id,
1412
+ collection: this.getEntityType() as UpdatePathValueData["collection"],
1413
+ path,
1414
+ value: value as UpdatePathValueData["value"],
1415
+ };
1416
+ if (opts.setType !== undefined) payload.setType = opts.setType;
1417
+ if (opts.arrayForm) payload.arrayForm = true;
1418
+ if (opts.pull) payload.pull = opts.pull;
1419
+ if (opts.updatePartial) payload.updatePartial = true;
1420
+ if (opts.formParentId) payload.formParentId = opts.formParentId;
1421
+ if (opts.edit) payload.edit = true;
1422
+
1423
+ // Normalisation wire format
1424
+ return this.endpointApi.updatePathValue(this._normalizeUpdatePathPayload(payload));
1425
+ }
1426
+
1427
+ /**
1428
+ * Normalise le payload `updatePathValue` avant envoi au backend.
1429
+ *
1430
+ * Règles appliquées (4) :
1431
+ * - **R0** : `value instanceof Date` → `value.toISOString()` + `setType: "isoDate"`
1432
+ * (override d'un setType existant — l'intent `Date` est prioritaire).
1433
+ * - **R1** : `setType: [{type:"isoDate"}, ...]` uniforme + `value: string` →
1434
+ * collapse en `setType: "isoDate"` (forme scalaire préférée backend).
1435
+ * - **R2** : `pull` présent + `value: ""|null|undefined` → force `value: ""`
1436
+ * (le backend exige `value: string` quand `pull` est présent — contrainte
1437
+ * schema `allOf`).
1438
+ * - **R6** : `setType: []` array vide → drop silencieusement (évite wire
1439
+ * format inutile, sécurise contre filtres qui ont tout retiré).
1440
+ *
1441
+ * Pour traiter une string ISO comme date, le caller doit passer `setType`
1442
+ * explicitement (`"isoDate"` ou `[{path, type:"isoDate"}]`). Pas d'heuristique
1443
+ * sur les strings pour éviter les faux positifs sur champs texte libre.
1444
+ *
1445
+ * @protected
1446
+ */
1447
+ protected _normalizeUpdatePathPayload(payload: UpdatePathValueData): UpdatePathValueData {
1448
+ let normalized = payload;
1449
+
1450
+ // R6 — drop setType array vide
1451
+ if (Array.isArray(normalized.setType) && normalized.setType.length === 0) {
1452
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1453
+ const { setType, ...rest } = normalized;
1454
+ normalized = rest as UpdatePathValueData;
1455
+ }
1456
+
1457
+ // R0 — Date instance → ISO + setType "isoDate"
1458
+ if (normalized.value instanceof Date) {
1459
+ normalized = {
1460
+ ...normalized,
1461
+ value: normalized.value.toISOString(),
1462
+ setType: "isoDate",
1463
+ };
1464
+ }
1465
+
1466
+ // R1 — setType array uniforme isoDate + value string → collapse scalaire
1467
+ const isIsoDateSetTypeArray =
1468
+ Array.isArray(normalized.setType)
1469
+ && normalized.setType.length > 0
1470
+ && normalized.setType.every(item => item?.type === "isoDate");
1471
+ if (typeof normalized.value === "string" && isIsoDateSetTypeArray) {
1472
+ normalized = { ...normalized, setType: "isoDate" };
1473
+ }
1474
+
1475
+ // R2 — pull + value empty → force ""
1476
+ if (normalized.pull
1477
+ && (normalized.value === "" || normalized.value === null || normalized.value === undefined)) {
1478
+ normalized = { ...normalized, value: "" };
1479
+ }
1480
+
1481
+ return normalized;
1482
+ }
1483
+
1262
1484
  /**
1263
1485
  * Valide les entrées d'upload de fichiers.
1264
1486
  *
@@ -5179,21 +5401,32 @@ export class BaseEntity<TServerData = any> {
5179
5401
  }
5180
5402
 
5181
5403
  /**
5182
- * Generer un nouveau identifiant unique pour un answers basé sur un formulaire donnée
5183
- *
5184
- * @param formId - Identifiant du formulaire pour lequel générer un nouvel ID d'answers
5185
- * @returns Un nouvel answers avec le nouveau ID généré
5186
- * @throws {ApiError} Si formId est absent ou invalide
5187
- *
5404
+ * Génère côté backend une nouvelle Answer placeholder rattachée au formulaire
5405
+ * donné (insert en BDD avec nouvel ObjectId) et retourne l'instance Answer
5406
+ * connectée (parent = `this`).
5407
+ *
5408
+ * **Note workflow** : l'Answer renvoyée est en état "placeholder" — pas de
5409
+ * `user` posé côté backend tant que `save()` n'a pas été appelé. Pour
5410
+ * récupérer plus tard via `coformAnswersById`, faire `save()` avec au moins
5411
+ * `answers` posés.
5412
+ *
5413
+ * @param formId - ID du formulaire (24 hex)
5414
+ * @returns Instance `Answer` connectée à `this` (parent = entité courante)
5415
+ * @throws {ApiError} 400 si `formId` est absent ou invalide
5416
+ *
5417
+ * @example
5418
+ * const org = await api.organization({ slug: "monOrga" });
5419
+ * const answer = await org.generateNewAnswerId(formId);
5420
+ * await answer.updateField(`${finder}.${org.id}`, { id: org.id, type: "organizations", name: org.serverData.name });
5188
5421
  */
5189
5422
  async generateNewAnswerId(
5190
5423
  formId: string
5191
- ): Promise<any> {
5424
+ ): Promise<Answer> {
5192
5425
  if (!formId || typeof formId !== "string") {
5193
5426
  throw new ApiError("formId est requis et doit être une chaîne de caractères.", 400);
5194
5427
  }
5195
5428
  const result = await this.endpointApi.generateAnswerFromForm({ pathParams: { formId }, action: "new" });
5196
- return this._linkEntity?.(result.collection, result) ?? result;
5429
+ return (this._linkEntity?.(result.collection, result) ?? result) as Answer;
5197
5430
  }
5198
5431
 
5199
5432
  /**
@@ -1,7 +1,7 @@
1
1
  import { ApiError } from "../error.js";
2
2
  import { BaseEntity } from "./BaseEntity.js";
3
3
 
4
- import type { AddCommentsData, AddReportAbuseData, AddVoteData, DeleteCommentsData, UpdateCommentsData } from "./EndpointApi.types.js";
4
+ import type { AddCommentsData, AddReportAbuseData, AddVoteData, DeleteCommentsData, ShowVoteData, UpdateCommentsData } from "./EndpointApi.types.js";
5
5
  import type { CommentItemNormalized } from "./serverDataType/Comment.js";
6
6
 
7
7
  export class Comment extends BaseEntity<CommentItemNormalized> {
@@ -222,6 +222,31 @@ export class Comment extends BaseEntity<CommentItemNormalized> {
222
222
  return await this.callIsConnected(() => this.endpointApi.addVote(payload));
223
223
  }
224
224
 
225
+ /**
226
+ * Récupère la liste des votes (like, love, etc.) sur ce commentaire.
227
+ *
228
+ * Wrap de `SHOW_VOTE` avec auto-injection du `type` (`"comments"`) et de
229
+ * `this.id` dans `pathParams`. Le retour contient `vote` (votes individuels
230
+ * indexés par userId) et `voteCount` (compteurs par statut).
231
+ *
232
+ * @returns Réponse API : `{ _id, vote, voteCount }` ou variante d'erreur.
233
+ * @throws {ApiError} 404 si le commentaire n'a pas d'id (non enregistré).
234
+ *
235
+ * @example
236
+ * const votes = await comment.getVotes();
237
+ * votes.voteCount?.like; // nombre de likes
238
+ * votes.vote?.[userId]; // vote détaillé d'un user
239
+ */
240
+ async getVotes(): Promise<unknown> {
241
+ if (!this.id) {
242
+ throw new ApiError(`${this.constructor.name} non enregistré.`, 404);
243
+ }
244
+ const payload: ShowVoteData = {
245
+ pathParams: { type: "comments", id: this.id },
246
+ };
247
+ return this.endpointApi.showVote(payload);
248
+ }
249
+
225
250
  /**
226
251
  * Signale un abus sur ce commentaire
227
252
  *
@@ -5639,6 +5639,18 @@ export interface UpdatePathValueData {
5639
5639
  }
5640
5640
  | unknown[]
5641
5641
  | null;
5642
+ /**
5643
+ * Mode multi-field merge : `value` doit être un objet, chaque clé devient un sub-path et est mise à jour individuellement (vs `$set` qui écrase l'objet entier). Les sous-clés à `null`/`""` sont supprimées (`$unset`). Combinable avec `setType: Array` pour coercion par sous-clé.
5644
+ */
5645
+ updatePartial?: boolean;
5646
+ /**
5647
+ * ID du Form parent. Utilisé côté backend pour : (1) authorisation hiérarchique sur sub-forms (admin du Form parent autorisé à modifier les sub-inputs), (2) invalidation du cache de traduction du Form parent après modification.
5648
+ */
5649
+ formParentId?: string;
5650
+ /**
5651
+ * Avec `arrayForm`, désactive le mode `$push` et force `$set` (écrase l'array entier). Cas legacy — préférer omettre `arrayForm` pour écraser un array.
5652
+ */
5653
+ edit?: boolean;
5642
5654
  [k: string]: unknown;
5643
5655
  }
5644
5656
 
package/src/api/News.ts CHANGED
@@ -2,7 +2,7 @@ import { ApiError, ApiResponseError } from "../error.js";
2
2
  import { BaseEntity } from "./BaseEntity.js";
3
3
 
4
4
  import type { Comment } from "./Comment.js";
5
- import type { AddNewsData, UpdateNewsData, DeleteNewsData, AddImageNewsData, AddFileNewsData, GetCommentsData, AddVoteData, AddReportAbuseData, ShareNewsData } from "./EndpointApi.types.js";
5
+ import type { AddNewsData, UpdateNewsData, DeleteNewsData, AddImageNewsData, AddFileNewsData, GetCommentsData, AddVoteData, AddReportAbuseData, ShareNewsData, ShowVoteData } from "./EndpointApi.types.js";
6
6
  import type { NewsItemNormalized } from "./serverDataType/News.js";
7
7
 
8
8
  export class News extends BaseEntity<NewsItemNormalized> {
@@ -366,6 +366,31 @@ export class News extends BaseEntity<NewsItemNormalized> {
366
366
  return await this.callIsConnected(() => this.endpointApi.addVote(payload));
367
367
  }
368
368
 
369
+ /**
370
+ * Récupère la liste des votes (like, love, etc.) sur cette news.
371
+ *
372
+ * Wrap de `SHOW_VOTE` avec auto-injection du `type` (`"news"`) et de
373
+ * `this.id` dans `pathParams`. Le retour contient `vote` (votes individuels
374
+ * indexés par userId) et `voteCount` (compteurs par statut).
375
+ *
376
+ * @returns Réponse API : `{ _id, vote, voteCount }` ou variante d'erreur.
377
+ * @throws {ApiError} 404 si la news n'a pas d'id (non enregistrée).
378
+ *
379
+ * @example
380
+ * const votes = await news.getVotes();
381
+ * votes.voteCount?.like; // nombre de likes
382
+ * votes.vote?.[userId]; // vote détaillé d'un user
383
+ */
384
+ async getVotes(): Promise<unknown> {
385
+ if (!this.id) {
386
+ throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
387
+ }
388
+ const payload: ShowVoteData = {
389
+ pathParams: { type: "news", id: this.id },
390
+ };
391
+ return this.endpointApi.showVote(payload);
392
+ }
393
+
369
394
  /**
370
395
  * Signale un abus sur cette news
371
396
  *
@@ -513,16 +513,7 @@ export class Organization extends BaseEntity<OrganizationItemNormalized> {
513
513
  * await org.updateOpeningHours(openingHours);
514
514
  */
515
515
  async updateOpeningHours(hours: OpeningHoursEntry[]): Promise<unknown> {
516
- if (!this.id) {
517
- throw new ApiError("L'organisation n'a pas d'ID, impossible de mettre à jour les horaires d'ouverture.", 400);
518
- }
519
-
520
- const result = await this.endpointApi.updatePathValue({
521
- id: this.id,
522
- collection: "organizations",
523
- path: "openingHours",
524
- value: hours as unknown as { [k: string]: unknown }
525
- });
516
+ const result = await this.updateField("openingHours", hours);
526
517
  await this._refreshIfDirect();
527
518
  return result;
528
519
  }