@communecter/cocolight-api-client 1.0.123 → 1.0.125

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.
@@ -49,7 +49,16 @@ import type {
49
49
  GetCountriesData,
50
50
  SearchZonesData,
51
51
  CoformAnswersByFormsData,
52
- FundingEnvelopeData
52
+ FundingEnvelopeData,
53
+ LinkDiscourseAccountData,
54
+ UnlinkDiscourseAccountData,
55
+ DiscourseProfileData,
56
+ DiscourseCheckEmailData,
57
+ DiscourseDismissLinkData,
58
+ LinkMediawikiAccountData,
59
+ UnlinkMediawikiAccountData,
60
+ GetMediawikiContributionsData,
61
+ CoremuOperationData
53
62
  } from "./EndpointApi.types.js";
54
63
  import type { GetElementsKeyResponse } from "../types/api-responses.js";
55
64
  import type { TransformsMap } from "../types/entities.js";
@@ -65,6 +74,7 @@ type OrganizationInput = { id: string } | { slug: string } | Record<string, any>
65
74
  type ProjectInput = { id: string } | { slug: string } | Record<string, any>;
66
75
  type PoiInput = { id: string } | { slug: string } | Record<string, any>;
67
76
  type EventInput = { id: string } | { slug: string } | Record<string, any>;
77
+ type ClassifiedInput = { id: string } | Record<string, any>;
68
78
  type BadgeInput = { id: string } | Record<string, any>;
69
79
  type NewsInput = { id: string } | Record<string, any>;
70
80
 
@@ -73,7 +83,7 @@ type NewsInput = { id: string } | Record<string, any>;
73
83
  * possédant au moins .toHexString().
74
84
  * (peu importe la d.ts du module, on impose notre contrat minimal)
75
85
  */
76
- const createObjectId: (arg?: number|string|number[]|Buffer) => { toHexString(): string } = objectId as any;
86
+ const createObjectId: (arg?: number | string | number[] | Buffer) => { toHexString(): string } = objectId as any;
77
87
 
78
88
  // Types TypeScript importés
79
89
  type ApiClient = import("../ApiClient.js").default;
@@ -88,9 +98,10 @@ type Badge = import("./Badge.js").Badge;
88
98
  type Comment = import("./Comment.js").Comment;
89
99
  type Answer = import("./Answer.js").Answer;
90
100
  type Form = import("./Form.js").Form;
101
+ type Classified = import("./Classified.js").Classified;
91
102
 
92
103
  // Types d'union
93
- type AnyEntity = User | Organization | Project | Poi | EventEntity | Badge | News | Comment | Answer | Form;
104
+ type AnyEntity = User | Organization | Project | Poi | EventEntity | Badge | News | Comment | Answer | Form | Classified;
94
105
  type ParentLike = BaseEntity<any> & { apiClient: ApiClient, userContext?: User | null };
95
106
 
96
107
  // Types pour les dépendances
@@ -109,6 +120,7 @@ interface Deps {
109
120
  Comment?: any;
110
121
  Answer?: any;
111
122
  Form?: any;
123
+ Classified?: any
112
124
  }
113
125
 
114
126
  interface BaseEntityConfig {
@@ -127,6 +139,7 @@ interface EntityTypeMap {
127
139
  comments: Comment;
128
140
  answers: Answer;
129
141
  forms: Form;
142
+ classifieds: Classified;
130
143
  }
131
144
 
132
145
  // Types pour les streams et uploads
@@ -135,6 +148,15 @@ type UploadInput = File | Blob | Buffer | import("stream").Readable;
135
148
  type ValidatedUpload = File | Buffer | ReadableWithMeta;
136
149
 
137
150
  // Type de retour pour fundingEnvelope
151
+ //
152
+ // Note: `projects` est un tableau d'enveloppes de financement.
153
+ // Chaque enveloppe contient les métadonnées (totalFinancement, depenses, actions, etc.)
154
+ // + un sous-champ `project` qui porte l'entité projet réelle.
155
+ export type FundingEnvelopeProjectItem = {
156
+ project?: AnyEntity | object;
157
+ [k: string]: unknown;
158
+ };
159
+
138
160
  export type FundingEnvelopeResult = {
139
161
  contextData?: AnyEntity | object;
140
162
  context?: AnyEntity | object;
@@ -142,11 +164,22 @@ export type FundingEnvelopeResult = {
142
164
  form?: object;
143
165
  paymentMethods?: object;
144
166
  nopropProject?: AnyEntity[] | object;
145
- projects?: AnyEntity[] | any[];
167
+ projects?: FundingEnvelopeProjectItem[];
146
168
  userOrga?: AnyEntity[] | object;
147
169
  [k: string]: unknown;
148
170
  };
149
171
 
172
+ // Champs auto-injectés par `_withCostumContext` dans le finalData reçu par le finalizer.
173
+ // Permet de typer correctement le param finalData côté callers sans cast `as any`.
174
+ export type CostumContextFields = {
175
+ costumSlug: string;
176
+ contextId: string;
177
+ contextType: EntityType;
178
+ sourceKey: string[];
179
+ };
180
+
181
+ export type WithCostumContext<T> = T & CostumContextFields;
182
+
150
183
  // Type pour le curseur de pagination utilisé dans _createPaginatorEngine
151
184
  type PaginationCursor = {
152
185
  searchType?: string[];
@@ -203,8 +236,8 @@ export interface PaginatorPage<T> {
203
236
  */
204
237
  export type ExtractMethodNames<T> = T extends Map<any, infer V>
205
238
  ? V extends readonly (readonly [any, infer M])[]
206
- ? M
207
- : never
239
+ ? M
240
+ : never
208
241
  : never;
209
242
 
210
243
  // LinkMeta interface
@@ -217,7 +250,7 @@ interface LinkMeta {
217
250
  // Type du constructeur de BaseEntity avec ses propriétés statiques
218
251
  type BaseEntityCtor = typeof BaseEntity & {
219
252
  entityTag: string;
220
- entityType: "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" | "answers";
253
+ entityType: "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" | "answers" | "forms" | "classifieds";
221
254
  SCHEMA_CONSTANTS: string | string[];
222
255
  };
223
256
 
@@ -311,7 +344,7 @@ interface FinalizerResult<TOut> {
311
344
  }
312
345
 
313
346
  // Types pour les entités
314
- type EntityType = "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" | "answers";
347
+ export type EntityType = "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" | "answers" | "forms" | "classifieds";
315
348
 
316
349
  // ============================================================================
317
350
 
@@ -488,11 +521,11 @@ export class BaseEntity<TServerData = any> {
488
521
 
489
522
  /** @returns Indique si cette entité représente l'utilisateur connecté */
490
523
  get isMe(): boolean {
491
- return !!(this.isConnected && this.userId && this.userContext?.id && typeof this.userId=== "string" && typeof this.userContext?.id === "string" && this.userId === this.userContext?.id);
524
+ return !!(this.isConnected && this.userId && this.userContext?.id && typeof this.userId === "string" && typeof this.userContext?.id === "string" && this.userId === this.userContext?.id);
492
525
  }
493
526
 
494
527
  /** @returns Type de l'entité (ex: 'citoyens') */
495
- getEntityType(): "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" | "answers" {
528
+ getEntityType(): "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" | "answers" | "forms" | "classifieds" {
496
529
  return this._getCtor().entityType;
497
530
  }
498
531
 
@@ -533,7 +566,7 @@ export class BaseEntity<TServerData = any> {
533
566
  await this._add(payload);
534
567
  this._resetInitialDraftData();
535
568
  // on refresh le contexte utilisateur si besoin
536
- if(this.userContext) {
569
+ if (this.userContext) {
537
570
  await this.userContext?.refresh();
538
571
  }
539
572
  return await this.refresh();
@@ -658,9 +691,9 @@ export class BaseEntity<TServerData = any> {
658
691
  const initialValue = this._initialDraftData?.[key];
659
692
 
660
693
  const isModified =
661
- current !== undefined &&
662
- initialValue !== undefined &&
663
- this._serialize(this._toRawDeep(current)) !== this._serialize(initialValue);
694
+ current !== undefined &&
695
+ initialValue !== undefined &&
696
+ this._serialize(this._toRawDeep(current)) !== this._serialize(initialValue);
664
697
 
665
698
  if (!isModified) {
666
699
  this._draftData[key] = draft[key];
@@ -862,8 +895,8 @@ export class BaseEntity<TServerData = any> {
862
895
 
863
896
  if (
864
897
  typeof val === "function" ||
865
- typeof val === "symbol" ||
866
- typeof val === "undefined"
898
+ typeof val === "symbol" ||
899
+ typeof val === "undefined"
867
900
  ) {
868
901
  continue;
869
902
  }
@@ -991,7 +1024,7 @@ export class BaseEntity<TServerData = any> {
991
1024
  * @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
992
1025
  */
993
1026
  async callNoConnected(constant: string, data: object = {}): Promise<unknown> {
994
- if(this.isConnected) {
1027
+ if (this.isConnected) {
995
1028
  throw new ApiAuthenticationError("Vous devez ne devez pas être connecté pour faire cette action.", 403);
996
1029
  }
997
1030
  return this.call(constant, data);
@@ -1009,7 +1042,7 @@ export class BaseEntity<TServerData = any> {
1009
1042
  * @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
1010
1043
  */
1011
1044
  async callIsConnected(param: string | (() => Promise<any>), data: object = {}): Promise<unknown> {
1012
- if(!this.isConnected) {
1045
+ if (!this.isConnected) {
1013
1046
  throw new ApiAuthenticationError("Vous devez être connecté pour faire cette action.", 401);
1014
1047
  }
1015
1048
  // Si le premier paramètre est une fonction, on l'exécute en tant que callback
@@ -1086,12 +1119,12 @@ export class BaseEntity<TServerData = any> {
1086
1119
  if (!input) {
1087
1120
  throw new ApiValidationError("Le fichier est requis.", 400, ["Le fichier est requis"]);
1088
1121
  }
1089
-
1122
+
1090
1123
  const isNode = typeof window === "undefined" && typeof process !== "undefined";
1091
1124
 
1092
1125
  let output: ValidatedUpload | null = null;
1093
1126
  let mimeType = "";
1094
-
1127
+
1095
1128
  // Navigateur : File
1096
1129
  if (typeof File !== "undefined" && input instanceof File) {
1097
1130
  mimeType = input.type;
@@ -1100,19 +1133,19 @@ export class BaseEntity<TServerData = any> {
1100
1133
  }
1101
1134
  output = input;
1102
1135
  }
1103
-
1136
+
1104
1137
  // Navigateur : Blob
1105
1138
  else if (typeof Blob !== "undefined" && input instanceof Blob) {
1106
1139
  mimeType = input.type;
1107
1140
  if (!allowedMimeTypes.includes(mimeType)) {
1108
1141
  throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
1109
1142
  }
1110
-
1143
+
1111
1144
  const ext = mimeType.split("/")[1] || "bin";
1112
1145
  const fileName = `${Date.now()}.${ext}`;
1113
1146
  output = new File([input], fileName, { type: mimeType });
1114
1147
  }
1115
-
1148
+
1116
1149
  // Node.js : Buffer
1117
1150
  else if (isNode && Buffer.isBuffer(input)) {
1118
1151
  const fileTypeResult = await fromBuffer(input);
@@ -1120,13 +1153,13 @@ export class BaseEntity<TServerData = any> {
1120
1153
  if (!fileTypeResult) {
1121
1154
  throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
1122
1155
  }
1123
-
1156
+
1124
1157
  mimeType = fileTypeResult.mime;
1125
-
1158
+
1126
1159
  if (!allowedMimeTypes.includes(mimeType)) {
1127
1160
  throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
1128
1161
  }
1129
-
1162
+
1130
1163
  // Pour un fichier image, on transforme en stream
1131
1164
  if (expectedType === "image") {
1132
1165
  const ext = fileTypeResult.ext;
@@ -1136,7 +1169,7 @@ export class BaseEntity<TServerData = any> {
1136
1169
  output = input;
1137
1170
  }
1138
1171
  }
1139
-
1172
+
1140
1173
  // Node.js : ReadableStream
1141
1174
  else if (isNode && isNodeReadable(input)) {
1142
1175
  const readableIn = input as import("stream").Readable;
@@ -1175,11 +1208,11 @@ export class BaseEntity<TServerData = any> {
1175
1208
 
1176
1209
  output = resultStream as ReadableWithMeta;
1177
1210
  }
1178
-
1211
+
1179
1212
  else {
1180
1213
  throw new ApiValidationError("Type de fichier non reconnu.", 400, ["Type de fichier non reconnu."]);
1181
1214
  }
1182
-
1215
+
1183
1216
  return output!;
1184
1217
  }
1185
1218
 
@@ -1214,7 +1247,7 @@ export class BaseEntity<TServerData = any> {
1214
1247
  throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
1215
1248
  }
1216
1249
  }
1217
-
1250
+
1218
1251
  /**
1219
1252
  * Crée un PassThrough stream pour le traitement des fichiers.
1220
1253
  *
@@ -1330,7 +1363,7 @@ export class BaseEntity<TServerData = any> {
1330
1363
  }
1331
1364
  });
1332
1365
  }
1333
-
1366
+
1334
1367
  /**
1335
1368
  * Génère un nouvel identifiant unique.
1336
1369
  * @returns Un identifiant unique.
@@ -1543,7 +1576,7 @@ export class BaseEntity<TServerData = any> {
1543
1576
  */
1544
1577
  private _buildDraftAndProxy({ data = {}, serverData = null, previousDraft = null, constant, apiClient, transforms = {}, throwOnError = true, removeFields = [] }: { data?: Record<string, unknown>; serverData?: Record<string, unknown> | null; previousDraft?: Record<string, unknown> | null; constant: string | string[]; apiClient: ApiClient; transforms?: TransformsMap; throwOnError?: boolean; removeFields?: string[] }): { draft: ReactiveData; proxy: Record<string, unknown>; initial: Record<string, unknown> } {
1545
1578
  const constants = Array.isArray(constant) ? constant : [constant];
1546
- const combinedSchema: {allOf: JsonSchema[], $defs: Record<string, JsonSchema>} = {
1579
+ const combinedSchema: { allOf: JsonSchema[], $defs: Record<string, JsonSchema> } = {
1547
1580
  allOf: [],
1548
1581
  $defs: {}
1549
1582
  };
@@ -1601,7 +1634,7 @@ export class BaseEntity<TServerData = any> {
1601
1634
  ? transforms[key](raw, data)
1602
1635
  : raw;
1603
1636
  return [key, transformed];
1604
-
1637
+
1605
1638
  }).filter(([_, v]) => v !== undefined)
1606
1639
  );
1607
1640
 
@@ -1618,7 +1651,7 @@ export class BaseEntity<TServerData = any> {
1618
1651
 
1619
1652
  const proxy = this._createDraftProxy(apiClient, reactiveServer, draft, allowed, transforms, { throwOnError });
1620
1653
 
1621
- return { draft, proxy, initial};
1654
+ return { draft, proxy, initial };
1622
1655
  }
1623
1656
 
1624
1657
  /**
@@ -1844,20 +1877,20 @@ export class BaseEntity<TServerData = any> {
1844
1877
  if (!this.id && this.slug) {
1845
1878
  try {
1846
1879
  const data = await this.endpointApi.getElementsKey({
1847
- pathParams:{
1880
+ pathParams: {
1848
1881
  slug: this.slug
1849
1882
  }
1850
1883
  }) as GetElementsKeyResponse;
1851
1884
 
1852
- if(data.contextId && data.contextType === type) {
1885
+ if (data.contextId && data.contextType === type) {
1853
1886
  this._id(data.contextId);
1854
1887
  } else {
1855
1888
  throw new ApiResponseError(`Le slug ${this.slug} ne correspond pas à un ${type}`, 200, data as object);
1856
1889
  }
1857
1890
  } catch (error) {
1858
- if(error instanceof ApiResponseError) {
1891
+ if (error instanceof ApiResponseError) {
1859
1892
  const errorResponseData = error.responseData as Record<string, unknown>;
1860
- if(errorResponseData.contextType !== type) {
1893
+ if (errorResponseData.contextType !== type) {
1861
1894
  throw error;
1862
1895
  } else {
1863
1896
  throw new ApiResponseError(`Impossible de récupérer l'identifiant pour le slug ${this.slug}`, error.status, error.responseData);
@@ -1872,7 +1905,7 @@ export class BaseEntity<TServerData = any> {
1872
1905
  }
1873
1906
  return this.id;
1874
1907
  }
1875
-
1908
+
1876
1909
  /**
1877
1910
  * Récupère le profil public de l'entité.
1878
1911
  *
@@ -1887,9 +1920,9 @@ export class BaseEntity<TServerData = any> {
1887
1920
  const type = this.getEntityType();
1888
1921
 
1889
1922
  if (type === "news" || type === "comments") {
1890
- throw new ApiError("getElementsAbout ne supporte pas le type 'news'.", 400);
1923
+ throw new ApiError(`getElementsAbout ne supporte pas le type '${type}'.`, 400);
1891
1924
  }
1892
- return this.endpointApi.getElementsAbout({ pathParams: { id: this.id, type: type }, tpl: "ficheInfoElement" });
1925
+ return this.endpointApi.getElementsAbout({ pathParams: { id: this.id, type }, tpl: "ficheInfoElement" });
1893
1926
  }
1894
1927
 
1895
1928
  /**
@@ -1913,24 +1946,28 @@ export class BaseEntity<TServerData = any> {
1913
1946
  Comment: selfTag === "Comment" ? selfClass : this.deps.Comment,
1914
1947
  Answer: selfTag === "Answer" ? selfClass : this.deps.Answer,
1915
1948
  Form: selfTag === "Form" ? selfClass : this.deps.Form,
1949
+ Classified: selfTag === "Classified" ? selfClass : this.deps.Classified,
1916
1950
  };
1917
1951
 
1918
1952
  const map = {
1919
- citoyens: { entityClass: commonDeps.User, deps: commonDeps },
1920
- organizations:{ entityClass: commonDeps.Organization, deps: commonDeps },
1921
- projects: { entityClass: commonDeps.Project, deps: commonDeps },
1922
- events: { entityClass: commonDeps.Event, deps: { ...commonDeps, Badge: undefined } },
1923
- poi: { entityClass: commonDeps.Poi, deps: { ...commonDeps, Badge: undefined, News: undefined } },
1924
- news: { entityClass: commonDeps.News, deps: { ...commonDeps } },
1925
- badges: { entityClass: commonDeps.Badge, deps: {
1926
- EndpointApi: commonDeps.EndpointApi,
1927
- User: commonDeps.User,
1928
- Organization: commonDeps.Organization,
1929
- Project: commonDeps.Project
1930
- } },
1931
- comments: { entityClass: commonDeps.Comment, deps: { ...commonDeps } },
1932
- answers: { entityClass: commonDeps.Answer, deps: { ...commonDeps } },
1933
- forms: { entityClass: commonDeps.Form, deps: { ...commonDeps } },
1953
+ classifieds: { entityClass: commonDeps.Classified, deps: { ...commonDeps } },
1954
+ citoyens: { entityClass: commonDeps.User, deps: commonDeps },
1955
+ organizations: { entityClass: commonDeps.Organization, deps: commonDeps },
1956
+ projects: { entityClass: commonDeps.Project, deps: commonDeps },
1957
+ events: { entityClass: commonDeps.Event, deps: { ...commonDeps, Badge: undefined } },
1958
+ poi: { entityClass: commonDeps.Poi, deps: { ...commonDeps, Badge: undefined, News: undefined } },
1959
+ news: { entityClass: commonDeps.News, deps: { ...commonDeps } },
1960
+ badges: {
1961
+ entityClass: commonDeps.Badge, deps: {
1962
+ EndpointApi: commonDeps.EndpointApi,
1963
+ User: commonDeps.User,
1964
+ Organization: commonDeps.Organization,
1965
+ Project: commonDeps.Project
1966
+ }
1967
+ },
1968
+ comments: { entityClass: commonDeps.Comment, deps: { ...commonDeps } },
1969
+ answers: { entityClass: commonDeps.Answer, deps: { ...commonDeps } },
1970
+ forms: { entityClass: commonDeps.Form, deps: { ...commonDeps } },
1934
1971
  };
1935
1972
 
1936
1973
  return (map as Record<string, EntityMeta | undefined>)[entityType] || null;
@@ -1980,11 +2017,11 @@ export class BaseEntity<TServerData = any> {
1980
2017
  * @return L'entité liée ou null.
1981
2018
  * @private
1982
2019
  */
1983
- async _linkEntityById(entityType: string, entityId: string, options?: { skipGet?: boolean }): Promise<AnyEntity|null> {
2020
+ async _linkEntityById(entityType: string, entityId: string, options?: { skipGet?: boolean }): Promise<AnyEntity | null> {
1984
2021
  const meta = this._getEntityMeta(entityType);
1985
2022
  if (!meta) return null;
1986
2023
 
1987
-
2024
+
1988
2025
  const parent: ParentLike = this as ParentLike;
1989
2026
 
1990
2027
  const entity = new meta.entityClass(parent, { id: entityId }, meta.deps) as AnyEntity;
@@ -2162,6 +2199,7 @@ export class BaseEntity<TServerData = any> {
2162
2199
  badges: ["id"],
2163
2200
  comments: [], // Pas de get() pour les commentaires
2164
2201
  answers: ["id"],
2202
+ classifieds: ["id"],
2165
2203
  };
2166
2204
 
2167
2205
  const fetchKeys = (fetchKeysByEntity as Record<string, string[] | undefined>)[entityType];
@@ -2190,12 +2228,12 @@ export class BaseEntity<TServerData = any> {
2190
2228
  }
2191
2229
  try {
2192
2230
  const data = await this.endpointApi.getElementsKey({
2193
- pathParams:{
2231
+ pathParams: {
2194
2232
  slug: slug
2195
2233
  }
2196
2234
  }) as GetElementsKeyResponse;
2197
2235
 
2198
- if(data.contextId && data.contextType) {
2236
+ if (data.contextId && data.contextType) {
2199
2237
  const entity = await this.entity(data.contextType, { id: data.contextId });
2200
2238
 
2201
2239
  // Corriger le userContext si nécessaire
@@ -2206,7 +2244,7 @@ export class BaseEntity<TServerData = any> {
2206
2244
  throw new ApiResponseError(`Le slug ${slug} n'est pas valide`, 200, data as object);
2207
2245
  }
2208
2246
  } catch (error) {
2209
- if(error instanceof ApiResponseError) {
2247
+ if (error instanceof ApiResponseError) {
2210
2248
  throw new ApiResponseError(`Impossible de récupérer l'identifiant pour le slug ${slug}`, error.status, error.responseData);
2211
2249
  } else {
2212
2250
  throw error;
@@ -2351,7 +2389,7 @@ export class BaseEntity<TServerData = any> {
2351
2389
  let hierarchyField: Record<string, any> | undefined;
2352
2390
  if (entityType === "events") {
2353
2391
  hierarchyField = entityData?.organizer;
2354
- } else if (entityType === "projects" || entityType === "poi") {
2392
+ } else if (entityType === "projects" || entityType === "poi" || entityType === "classifieds") {
2355
2393
  hierarchyField = entityData?.parent;
2356
2394
  } else {
2357
2395
  // Organizations et autres types n'ont pas de hiérarchie parent
@@ -2406,8 +2444,8 @@ export class BaseEntity<TServerData = any> {
2406
2444
 
2407
2445
  const parentLink = parentLinks[parentId];
2408
2446
  return this._validateUserLink(parentLink) &&
2409
- parentLink?.isAdmin === true &&
2410
- !parentLink?.isAdminPending;
2447
+ parentLink?.isAdmin === true &&
2448
+ !parentLink?.isAdminPending;
2411
2449
  }
2412
2450
 
2413
2451
  /**
@@ -2476,7 +2514,7 @@ export class BaseEntity<TServerData = any> {
2476
2514
  filters[`${path}.isAdmin`] = { $exists: true };
2477
2515
  }
2478
2516
 
2479
- if(isAdminPending === true) {
2517
+ if (isAdminPending === true) {
2480
2518
  filters[`${path}.isAdminPending`] = { $exists: true };
2481
2519
  }
2482
2520
 
@@ -2552,7 +2590,7 @@ export class BaseEntity<TServerData = any> {
2552
2590
  */
2553
2591
  protected _getLinkFromConnectedUser() {
2554
2592
  const { linkType } = this._getLinkMeta();
2555
- if(!this.id) {
2593
+ if (!this.id) {
2556
2594
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
2557
2595
  }
2558
2596
  return this?.userContext?.serverData?.links?.[linkType]?.[this.id] || null;
@@ -2572,7 +2610,7 @@ export class BaseEntity<TServerData = any> {
2572
2610
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
2573
2611
  }
2574
2612
 
2575
- if(!this.userId) {
2613
+ if (!this.userId) {
2576
2614
  throw new ApiError("L'utilisateur connecté n'est pas défini.", 400);
2577
2615
  }
2578
2616
 
@@ -2759,7 +2797,7 @@ export class BaseEntity<TServerData = any> {
2759
2797
  // Normalement en auto dans le schema mais je le met quand même
2760
2798
  connectType: connectTypeDisconnect
2761
2799
  };
2762
-
2800
+
2763
2801
  // TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
2764
2802
  const retour = await this.callIsMe(() => this.endpointApi.disconnect(data));
2765
2803
  await this.userContext?.refresh();
@@ -2784,7 +2822,7 @@ export class BaseEntity<TServerData = any> {
2784
2822
  return data;
2785
2823
  });
2786
2824
  }
2787
-
2825
+
2788
2826
  /**
2789
2827
  * Mettre à jour les paramètres d'un élément : Mise à jour des paramètres spécifiques d'un élément.
2790
2828
  * Constant : UPDATE_SETTINGS
@@ -2819,7 +2857,7 @@ export class BaseEntity<TServerData = any> {
2819
2857
 
2820
2858
  return this.callIsConnected(() => this.endpointApi.updateSettings(payload));
2821
2859
  }
2822
-
2860
+
2823
2861
  /**
2824
2862
  * Mettre à jour la description d'un élément : Permet de mettre à jour la description courte et complète d'un élément.
2825
2863
  * Constant : UPDATE_BLOCK_DESCRIPTION
@@ -2848,7 +2886,7 @@ export class BaseEntity<TServerData = any> {
2848
2886
 
2849
2887
  return this.callIsConnected(() => this.endpointApi.updateBlockDescription(payload));
2850
2888
  }
2851
-
2889
+
2852
2890
  /**
2853
2891
  * Mettre à jour les informations d'un élément : Permet de mettre à jour les informations générales d'un élément (nom, contacts, etc.).
2854
2892
  * Constant : UPDATE_BLOCK_INFO
@@ -2876,7 +2914,7 @@ export class BaseEntity<TServerData = any> {
2876
2914
  };
2877
2915
  return this.callIsConnected(() => this.endpointApi.updateBlockInfo(payload));
2878
2916
  }
2879
-
2917
+
2880
2918
  /**
2881
2919
  * Mettre à jour les réseaux sociaux d'un élément : Permet de mettre à jour les liens vers les réseaux sociaux d'un élément.
2882
2920
  * Constant : UPDATE_BLOCK_SOCIAL
@@ -2924,9 +2962,9 @@ export class BaseEntity<TServerData = any> {
2924
2962
  */
2925
2963
  private _isGeo(a: any): a is Geo {
2926
2964
  return !!a
2927
- && typeof a === "object"
2928
- && (typeof a.latitude === "number" || typeof a.latitude === "string")
2929
- && (typeof a.longitude === "number" || typeof a.longitude === "string");
2965
+ && typeof a === "object"
2966
+ && (typeof a.latitude === "number" || typeof a.latitude === "string")
2967
+ && (typeof a.longitude === "number" || typeof a.longitude === "string");
2930
2968
  }
2931
2969
 
2932
2970
  /**
@@ -2934,13 +2972,13 @@ export class BaseEntity<TServerData = any> {
2934
2972
  */
2935
2973
  private _isGeoPosition(a: any): a is GeoPosition {
2936
2974
  return !!a
2937
- && typeof a === "object"
2938
- && a.type === "Point"
2939
- && Array.isArray(a.coordinates)
2940
- && a.coordinates.length === 2
2941
- && (typeof a.coordinates[0] === "number" || typeof a.coordinates[0] === "string")
2942
- && (typeof a.coordinates[1] === "number" || typeof a.coordinates[1] === "string")
2943
- && a.float === true;
2975
+ && typeof a === "object"
2976
+ && a.type === "Point"
2977
+ && Array.isArray(a.coordinates)
2978
+ && a.coordinates.length === 2
2979
+ && (typeof a.coordinates[0] === "number" || typeof a.coordinates[0] === "string")
2980
+ && (typeof a.coordinates[1] === "number" || typeof a.coordinates[1] === "string")
2981
+ && a.float === true;
2944
2982
  }
2945
2983
 
2946
2984
  /**
@@ -2982,13 +3020,13 @@ export class BaseEntity<TServerData = any> {
2982
3020
  typeElement,
2983
3021
  address: addr,
2984
3022
  // On ne recopie que les champs autorisés pour éviter de casser le typage
2985
- ...( "geo" in data && this._isGeo(data.geo) ? { geo: data.geo } : {} ),
2986
- ...( "geoPosition" in data && this._isGeoPosition(data.geoPosition) ? { geoPosition: data.geoPosition } : {} ),
2987
- ...( "locality" in data ? { locality: data.locality } : {} ),
3023
+ ...("geo" in data && this._isGeo(data.geo) ? { geo: data.geo } : {}),
3024
+ ...("geoPosition" in data && this._isGeoPosition(data.geoPosition) ? { geoPosition: data.geoPosition } : {}),
3025
+ ...("locality" in data ? { locality: data.locality } : {}),
2988
3026
  };
2989
3027
  return this.callIsConnected(() => this.endpointApi.updateBlockLocality(payload));
2990
3028
  }
2991
-
3029
+
2992
3030
  /**
2993
3031
  * Mettre à jour le slug d'un élément : Permet de mettre à jour le slug pour une URL simplifiée.
2994
3032
  * Constant : UPDATE_BLOCK_SLUG
@@ -3009,7 +3047,7 @@ export class BaseEntity<TServerData = any> {
3009
3047
  try {
3010
3048
  await this.endpointApi.check({ block: "info", type, id: this.id, slug });
3011
3049
  } catch (error) {
3012
- if(error instanceof ApiResponseError) {
3050
+ if (error instanceof ApiResponseError) {
3013
3051
  throw new ApiResponseError("Erreur lors de la vérification du slug.", error.status, error.responseData);
3014
3052
  }
3015
3053
  throw error;
@@ -3027,7 +3065,7 @@ export class BaseEntity<TServerData = any> {
3027
3065
  * @param data.profil_avatar - L'image de profil à mettre à jour.
3028
3066
  * @returns - Les données de réponse.
3029
3067
  */
3030
- async updateImageProfil({ profil_avatar: image }: { profil_avatar: UploadInput }): Promise<unknown> {
3068
+ async updateImageProfil({ profil_avatar: image }: { profil_avatar: UploadInput }): Promise<unknown> {
3031
3069
 
3032
3070
  if (!this.id) {
3033
3071
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
@@ -3039,7 +3077,7 @@ export class BaseEntity<TServerData = any> {
3039
3077
 
3040
3078
  image = await this._validateImage(image);
3041
3079
 
3042
- const data: ProfilImageData = { pathParams: { folder, ownerId: this.id }, profil_avatar: image as unknown as Record<string, unknown>, };
3080
+ const data: ProfilImageData = { pathParams: { folder, ownerId: this.id }, profil_avatar: image as unknown as Record<string, unknown>, };
3043
3081
  return this.callIsConnected(() => this.endpointApi.profilImage(data));
3044
3082
  }
3045
3083
 
@@ -3054,7 +3092,7 @@ export class BaseEntity<TServerData = any> {
3054
3092
  * @param data.cropY - Position Y du recadrage.
3055
3093
  * @returns - Les données de réponse.
3056
3094
  */
3057
- async updateImageBanner({ banner: image, cropW, cropH, cropX, cropY }: { banner: UploadInput, cropW: number, cropH: number, cropX: number, cropY: number }): Promise<unknown> {
3095
+ async updateImageBanner({ banner: image, cropW, cropH, cropX, cropY }: { banner: UploadInput, cropW: number, cropH: number, cropX: number, cropY: number }): Promise<unknown> {
3058
3096
 
3059
3097
  if (!this.id) {
3060
3098
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
@@ -3080,7 +3118,7 @@ export class BaseEntity<TServerData = any> {
3080
3118
  * @throws {Error} Si une erreur se produit lors de la création de l'organisation.
3081
3119
  */
3082
3120
  async organization(organizationData: OrganizationInput = {}): Promise<Organization> {
3083
- if(!("id" in organizationData) && !("slug" in organizationData) && !this.isMe){
3121
+ if (!("id" in organizationData) && !("slug" in organizationData) && !this.isMe) {
3084
3122
  throw new ApiError("Vous devez être connecté et être l'utilisateur pour créer une organisation.", 403);
3085
3123
  }
3086
3124
  const entity = await this.entity("organizations", organizationData);
@@ -3098,7 +3136,7 @@ export class BaseEntity<TServerData = any> {
3098
3136
  */
3099
3137
  async project(projectData: ProjectInput = {}): Promise<Project> {
3100
3138
  // TODO: Vérifier si l'utilisateur est admin de l'organisation
3101
- if(!("id" in projectData) && !("slug" in projectData) && !this.isConnected){
3139
+ if (!("id" in projectData) && !("slug" in projectData) && !this.isConnected) {
3102
3140
  throw new ApiError("Vous devez être connecté pour créer un projet.", 401);
3103
3141
  }
3104
3142
 
@@ -3117,7 +3155,7 @@ export class BaseEntity<TServerData = any> {
3117
3155
  */
3118
3156
  async poi(poiData: PoiInput = {}): Promise<Poi> {
3119
3157
  // TODO: Vérifier si l'utilisateur est admin de l'organisation
3120
- if(!("id" in poiData) && !("slug" in poiData) && !this.isConnected){
3158
+ if (!("id" in poiData) && !("slug" in poiData) && !this.isConnected) {
3121
3159
  throw new ApiError("Vous devez être connecté pour créer un POI.", 401);
3122
3160
  }
3123
3161
 
@@ -3125,6 +3163,24 @@ export class BaseEntity<TServerData = any> {
3125
3163
  return entity as Poi;
3126
3164
  }
3127
3165
 
3166
+ /**
3167
+ * Crée une instance de ressource classifiée (besoin ou offre).
3168
+ *
3169
+ * @param classifiedData - Les données pour initialiser la ressource.
3170
+ * - Si { id } : récupère la ressource existante (GET)
3171
+ * - Sinon : crée une nouvelle instance (CREATE)
3172
+ * @returns Une promesse qui résout l'objet Classified créé.
3173
+ * @throws {Error} Si une erreur se produit lors de la création.
3174
+ */
3175
+ async classified(classifiedData: ClassifiedInput = {}): Promise<Classified> {
3176
+ if (!("id" in classifiedData) && !this.isConnected) {
3177
+ throw new ApiError("Vous devez être connecté pour créer une ressource.", 401);
3178
+ }
3179
+
3180
+ const entity = await this.entity("classifieds", classifiedData);
3181
+ return entity as Classified;
3182
+ }
3183
+
3128
3184
  /**
3129
3185
  * Crée une instance d'événement et la récupère si nécessaire.
3130
3186
  *
@@ -3136,7 +3192,7 @@ export class BaseEntity<TServerData = any> {
3136
3192
  */
3137
3193
  async event(eventData: EventInput = {}): Promise<EventEntity> {
3138
3194
  // TODO: Vérifier si l'utilisateur est admin de l'organisation
3139
- if(!("id" in eventData) && !("slug" in eventData) && !this.isConnected){
3195
+ if (!("id" in eventData) && !("slug" in eventData) && !this.isConnected) {
3140
3196
  throw new ApiError("Vous devez être connecté pour créer un événement.", 401);
3141
3197
  }
3142
3198
  const entity = await this.entity("events", eventData);
@@ -3154,7 +3210,7 @@ export class BaseEntity<TServerData = any> {
3154
3210
  */
3155
3211
  async badge(badgeData: BadgeInput = {}): Promise<Badge> {
3156
3212
  // TODO: Vérifier si l'utilisateur est admin de l'organisation
3157
- if(!("id" in badgeData) && !this.isConnected){
3213
+ if (!("id" in badgeData) && !this.isConnected) {
3158
3214
  throw new ApiError("Vous devez être connecté pour créer un badge.", 401);
3159
3215
  }
3160
3216
  const entity = await this.entity("badges", badgeData);
@@ -3171,7 +3227,7 @@ export class BaseEntity<TServerData = any> {
3171
3227
  * @throws {Error} Si une erreur se produit lors de la création de la news.
3172
3228
  */
3173
3229
  async news(newsData: NewsInput = {}): Promise<News> {
3174
- if(!newsData?.id && !this.isConnected){
3230
+ if (!newsData?.id && !this.isConnected) {
3175
3231
  throw new ApiError("Vous devez être connecté.", 401);
3176
3232
  }
3177
3233
 
@@ -3197,14 +3253,14 @@ export class BaseEntity<TServerData = any> {
3197
3253
  methodName: "getOrganizations",
3198
3254
  restoredState: options?.restoredState,
3199
3255
  finalizer: async (finalData) => {
3200
- if(this.isMe){
3256
+ if (this.isMe) {
3201
3257
  finalData.pathParams = { type: this.getEntityType(), id: this.id };
3202
- // NOTE : dans le schema je crois que si pas de finalData.filters alors le default ce fait avec finalData.pathParams
3203
- // finalData.filters = {
3204
- // [`links.members.${this.id}`]: { "$exists": true },
3205
- // [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
3206
- // [`links.members.${this.id}.isInviting`]: { "$exists": false }
3207
- // };
3258
+ // NOTE : dans le schema je crois que si pas de finalData.filters alors le default ce fait avec finalData.pathParams
3259
+ // finalData.filters = {
3260
+ // [`links.members.${this.id}`]: { "$exists": true },
3261
+ // [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
3262
+ // [`links.members.${this.id}.isInviting`]: { "$exists": false }
3263
+ // };
3208
3264
  } else {
3209
3265
  delete finalData?.pathParams;
3210
3266
  finalData.filters = {
@@ -3213,7 +3269,7 @@ export class BaseEntity<TServerData = any> {
3213
3269
  [`links.members.${this.id}.isInviting`]: { "$exists": false }
3214
3270
  };
3215
3271
  }
3216
-
3272
+
3217
3273
  const fetchFn = this.isMe
3218
3274
  ? () => this.callIsMe(() => this.endpointApi.getOrganizationsAdmin(finalData))
3219
3275
  : () => this.endpointApi.getOrganizationsNoAdmin(finalData);
@@ -3243,7 +3299,7 @@ export class BaseEntity<TServerData = any> {
3243
3299
  methodName: "getProjects",
3244
3300
  restoredState: options?.restoredState,
3245
3301
  finalizer: async (finalData) => {
3246
- if(this.isMe){
3302
+ if (this.isMe) {
3247
3303
  finalData.pathParams = { type: this.getEntityType(), id: this.id };
3248
3304
  finalData.filters = {
3249
3305
  "$or": {
@@ -3262,7 +3318,7 @@ export class BaseEntity<TServerData = any> {
3262
3318
  [`links.contributors.${this.id}`]: { "$exists": true }
3263
3319
  };
3264
3320
  }
3265
-
3321
+
3266
3322
  const fetchFn = this.isMe
3267
3323
  ? () => this.callIsMe(() => this.endpointApi.getProjectsAdmin(finalData))
3268
3324
  : () => this.endpointApi.getProjectsNoAdmin(finalData);
@@ -3323,7 +3379,7 @@ export class BaseEntity<TServerData = any> {
3323
3379
  methodName: "getPois",
3324
3380
  restoredState: options?.restoredState,
3325
3381
  finalizer: async (finalData) => {
3326
- if(this.isMe){
3382
+ if (this.isMe) {
3327
3383
  finalData.pathParams = { type: this.getEntityType(), id: this.id };
3328
3384
  // NOTE : dans le schema je crois que si pas de finalData.filters alors le default ce fait avec finalData.pathParams
3329
3385
  // finalData.filters = {
@@ -3335,7 +3391,7 @@ export class BaseEntity<TServerData = any> {
3335
3391
  [`parent.${this.id}`]: { "$exists": true },
3336
3392
  };
3337
3393
  }
3338
-
3394
+
3339
3395
  const fetchFn = this.isMe
3340
3396
  ? () => this.callIsMe(() => this.endpointApi.getPoisAdmin(finalData))
3341
3397
  : () => this.endpointApi.getPoisNoAdmin(finalData);
@@ -3367,7 +3423,7 @@ export class BaseEntity<TServerData = any> {
3367
3423
  finalizer: async (finalData) => {
3368
3424
  delete finalData?.pathParams;
3369
3425
 
3370
- finalData.filters = {
3426
+ finalData.filters = {
3371
3427
  [`links.follows.${this.id}`]: { "$exists": true },
3372
3428
  [`links.follows.${this.id}.toBeValidated`]: { "$exists": false },
3373
3429
  [`links.follows.${this.id}.isInviting`]: { "$exists": false }
@@ -3399,7 +3455,7 @@ export class BaseEntity<TServerData = any> {
3399
3455
  restoredState: options?.restoredState,
3400
3456
  finalizer: async (finalData) => {
3401
3457
  delete finalData?.pathParams;
3402
-
3458
+
3403
3459
  finalData.filters = finalData.filters || { "preferences.private": false };
3404
3460
  finalData.filters["$or"] = {};
3405
3461
  finalData.filters["$or"][`issuer.${this.id}`] = { "$exists": true };
@@ -3440,8 +3496,8 @@ export class BaseEntity<TServerData = any> {
3440
3496
  ...search
3441
3497
  };
3442
3498
 
3443
- const arrayObjet = await this.endpointApi.getNews(payload);
3444
- if(!Array.isArray(arrayObjet)){
3499
+ const arrayObjet = await this.endpointApi.getNews(payload);
3500
+ if (!Array.isArray(arrayObjet)) {
3445
3501
  throw new ApiResponseError("Erreur lors de la récupération des actualités.", 500, arrayObjet as object);
3446
3502
  }
3447
3503
 
@@ -3472,7 +3528,7 @@ export class BaseEntity<TServerData = any> {
3472
3528
  pathParams: { type, id: this.id, docType: data.pathParams?.docType || "image" }
3473
3529
  };
3474
3530
 
3475
- const arrayObjet = await this.endpointApi.getGallery(payload);
3531
+ const arrayObjet = await this.endpointApi.getGallery(payload);
3476
3532
 
3477
3533
  return arrayObjet as Record<string, any>;
3478
3534
  }
@@ -3530,7 +3586,7 @@ export class BaseEntity<TServerData = any> {
3530
3586
  const rawList = this._linkEntities(result);
3531
3587
  return rawList;
3532
3588
  }
3533
-
3589
+
3534
3590
  /**
3535
3591
  * Soumet une demande pour rejoindre l'entité courante (ex. organisation, projet, événement...).
3536
3592
  * Si une invitation est en attente, elle est automatiquement acceptée.
@@ -3581,7 +3637,7 @@ export class BaseEntity<TServerData = any> {
3581
3637
  }
3582
3638
 
3583
3639
  this._checkLinkableEntity();
3584
-
3640
+
3585
3641
  if (!this.id) {
3586
3642
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
3587
3643
  }
@@ -3673,7 +3729,7 @@ export class BaseEntity<TServerData = any> {
3673
3729
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
3674
3730
  }
3675
3731
 
3676
- if(!this.userId) {
3732
+ if (!this.userId) {
3677
3733
  throw new ApiError("Utilisateur non connecté.", 401);
3678
3734
  }
3679
3735
 
@@ -3715,7 +3771,7 @@ export class BaseEntity<TServerData = any> {
3715
3771
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
3716
3772
  }
3717
3773
 
3718
- if(!this.userId) {
3774
+ if (!this.userId) {
3719
3775
  throw new ApiError("Utilisateur non connecté.", 401);
3720
3776
  }
3721
3777
 
@@ -3741,7 +3797,7 @@ export class BaseEntity<TServerData = any> {
3741
3797
 
3742
3798
  throw new ApiError("Vous n'êtes pas abonné à cette entité.", 404);
3743
3799
  }
3744
-
3800
+
3745
3801
 
3746
3802
  /**
3747
3803
  * Vérifie si l'utilisateur est connecté et a accès à l'entité.
@@ -3840,7 +3896,7 @@ export class BaseEntity<TServerData = any> {
3840
3896
  * @protected
3841
3897
  */
3842
3898
  protected _isLinked(linkType: string): boolean {
3843
- if(!this.id) {
3899
+ if (!this.id) {
3844
3900
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
3845
3901
  }
3846
3902
  const links = this.userContext?.serverData?.links;
@@ -3894,7 +3950,7 @@ export class BaseEntity<TServerData = any> {
3894
3950
  isAdmin(options?: { checkHierarchy?: boolean; silent?: boolean }): boolean {
3895
3951
  const userLink = this._getValidatedUserLink(
3896
3952
  "vérifier l'administrateur",
3897
- ["organizations", "projects", "events"],
3953
+ ["organizations", "projects", "events", "classifieds"],
3898
3954
  { silent: options?.silent }
3899
3955
  );
3900
3956
 
@@ -4286,12 +4342,7 @@ export class BaseEntity<TServerData = any> {
4286
4342
  * Injection de contexte Communecter dans une requête finalizer.
4287
4343
  */
4288
4344
  _withCostumContext<TIn extends Record<string, unknown>, TOut>(
4289
- baseFinalizer: (data: TIn & {
4290
- costumSlug: string,
4291
- contextId: string,
4292
- contextType: EntityType,
4293
- sourceKey: string[]
4294
- }) => Promise<TOut>
4345
+ baseFinalizer: (data: TIn & CostumContextFields) => Promise<TOut>
4295
4346
  ): (data: TIn) => Promise<TOut> {
4296
4347
  return async (data: TIn) => {
4297
4348
  if (!this.serverData) {
@@ -4317,12 +4368,7 @@ export class BaseEntity<TServerData = any> {
4317
4368
  contextId: sd.id,
4318
4369
  contextType: this.getEntityType(),
4319
4370
  sourceKey: [...inSourceKey, sd.slug] as string[]
4320
- } as TIn & {
4321
- costumSlug: string;
4322
- contextId: string;
4323
- contextType: EntityType;
4324
- sourceKey: string[];
4325
- };
4371
+ } as TIn & CostumContextFields;
4326
4372
 
4327
4373
  return baseFinalizer(finalData);
4328
4374
  };
@@ -4390,7 +4436,7 @@ export class BaseEntity<TServerData = any> {
4390
4436
  */
4391
4437
  async costumEventRequestActors(data: Partial<Omit<CostumEventRequestActorsData, "pathParams">> = {}): Promise<unknown> {
4392
4438
 
4393
- if(!this.id) {
4439
+ if (!this.id) {
4394
4440
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4395
4441
  }
4396
4442
 
@@ -4438,7 +4484,7 @@ export class BaseEntity<TServerData = any> {
4438
4484
  */
4439
4485
  async costumEventRequestSubevents(data: Partial<Omit<CostumEventRequestSubeventsData, "pathParams">> = {}): Promise<unknown> {
4440
4486
 
4441
- if(!this.id) {
4487
+ if (!this.id) {
4442
4488
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4443
4489
  }
4444
4490
 
@@ -4468,7 +4514,7 @@ export class BaseEntity<TServerData = any> {
4468
4514
  */
4469
4515
  async costumEventRequestDates(data: Partial<Omit<CostumEventRequestDatesData, "pathParams">> = {}): Promise<unknown> {
4470
4516
 
4471
- if(!this.id) {
4517
+ if (!this.id) {
4472
4518
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4473
4519
  }
4474
4520
 
@@ -4498,7 +4544,7 @@ export class BaseEntity<TServerData = any> {
4498
4544
  */
4499
4545
  async costumEventRequestElementEvent(data: Partial<Omit<CostumEventRequestElementEventData, "pathParams">> = {}): Promise<unknown> {
4500
4546
 
4501
- if(!this.id) {
4547
+ if (!this.id) {
4502
4548
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4503
4549
  }
4504
4550
 
@@ -4528,7 +4574,7 @@ export class BaseEntity<TServerData = any> {
4528
4574
  */
4529
4575
  async costumEventRequestCategories(data: Partial<Omit<CostumEventRequestCategoriesData, "pathParams">> = {}): Promise<unknown> {
4530
4576
 
4531
- if(!this.id) {
4577
+ if (!this.id) {
4532
4578
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4533
4579
  }
4534
4580
 
@@ -4558,7 +4604,7 @@ export class BaseEntity<TServerData = any> {
4558
4604
  */
4559
4605
  async costumEventRequestEvent(data: Partial<Omit<CostumEventRequestEventData, "pathParams">> = {}): Promise<unknown> {
4560
4606
 
4561
- if(!this.id) {
4607
+ if (!this.id) {
4562
4608
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4563
4609
  }
4564
4610
 
@@ -4588,18 +4634,18 @@ export class BaseEntity<TServerData = any> {
4588
4634
  */
4589
4635
  async costumEventRequestLinkTlToEvent(
4590
4636
  data: Pick<CostumEventRequestLinkTlToEventData, "tl" | "event"> &
4591
- Partial<Omit<CostumEventRequestLinkTlToEventData, "tl" | "event" | "pathParams">>
4637
+ Partial<Omit<CostumEventRequestLinkTlToEventData, "tl" | "event" | "pathParams">>
4592
4638
  ): Promise<unknown> {
4593
4639
 
4594
- if(!this.id) {
4640
+ if (!this.id) {
4595
4641
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4596
4642
  }
4597
4643
 
4598
- if(!data.tl && !data.event) {
4644
+ if (!data.tl && !data.event) {
4599
4645
  throw new ApiError("Les paramètres 'tl' et 'event' sont requis.", 400);
4600
4646
  }
4601
4647
 
4602
- if(typeof data.tl !== "string" || typeof data.event !== "string") {
4648
+ if (typeof data.tl !== "string" || typeof data.event !== "string") {
4603
4649
  throw new ApiError("Les paramètres 'tl' et 'event' doivent être des chaînes de caractères.", 400);
4604
4650
  }
4605
4651
 
@@ -4629,7 +4675,7 @@ export class BaseEntity<TServerData = any> {
4629
4675
  */
4630
4676
  async costumEventRequestLoadContextTag(data: Partial<Omit<CostumEventRequestLoadContextTagData, "pathParams">> = {}): Promise<unknown> {
4631
4677
 
4632
- if(!this.id) {
4678
+ if (!this.id) {
4633
4679
  throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
4634
4680
  }
4635
4681
 
@@ -4648,7 +4694,7 @@ export class BaseEntity<TServerData = any> {
4648
4694
  return wrappedFinalizer(fullData);
4649
4695
  }
4650
4696
 
4651
-
4697
+
4652
4698
  /**
4653
4699
  * Recherche paginée des réponses CoForm liées à l'entité courante.
4654
4700
  * Injecte automatiquement le contexte (costumSlug, contextId, contextType) via `_withCostumContext`,
@@ -4714,8 +4760,8 @@ export class BaseEntity<TServerData = any> {
4714
4760
  */
4715
4761
  async searchAnswersByForms(
4716
4762
  data: Partial<CoformAnswersByFormsData> = {},
4717
- ): Promise<{ answers: Answer[]; documents: any[]; [k: string]: unknown }[]> {
4718
- if(!data.forms || Array.isArray(data.forms) || Object.keys(data.forms).length === 0){
4763
+ ): Promise<{ answers: Answer[]; documents: any[];[k: string]: unknown }[]> {
4764
+ if (!data.forms || Array.isArray(data.forms) || Object.keys(data.forms).length === 0) {
4719
4765
  throw new ApiError("Le paramètre 'forms' est requis et doit être un objet non vide.", 400);
4720
4766
  }
4721
4767
  const result = await this.endpointApi.coformAnswersByForms(data);
@@ -4761,7 +4807,20 @@ export class BaseEntity<TServerData = any> {
4761
4807
  ...(result.contextData && { contextData: this._linkEntity(result.contextData.collection, result.contextData) ?? result.contextData }),
4762
4808
  ...(result.context && { context: this._linkEntity(result.context.collection, result.context) ?? result.context }),
4763
4809
  ...(result.nopropProject && { nopropProject: this._linkEntities(Object.values(result.nopropProject)) ?? result.nopropProject }),
4764
- ...(result.projects && { projects: this._linkEntities(result.projects) ?? result.projects }),
4810
+ // `result.projects` est un tableau d'enveloppes la vraie entité projet est à
4811
+ // `result.projects[i].project`, pas à la racine de l'item (qui ne porte que les
4812
+ // métadonnées de financement). On linke chaque sous-champ `project` individuellement
4813
+ // pour récupérer des instances d'entités réactives, tout en préservant les méta.
4814
+ ...(result.projects && {
4815
+ projects: (result.projects as FundingEnvelopeProjectItem[]).map((envelope) => {
4816
+ const inner = envelope?.project as { collection?: string } | undefined;
4817
+ if (inner && typeof inner === "object") {
4818
+ const linked = this._linkEntity(inner.collection ?? "projects", inner);
4819
+ return { ...envelope, project: linked ?? envelope.project };
4820
+ }
4821
+ return envelope;
4822
+ })
4823
+ }),
4765
4824
  ...(result.userOrga && { userOrga: this._linkEntities(Object.values(result.userOrga)) ?? result.userOrga })
4766
4825
  };
4767
4826
  }
@@ -4814,7 +4873,7 @@ export class BaseEntity<TServerData = any> {
4814
4873
  async searchZone(
4815
4874
  data: SearchZonesData,
4816
4875
  ): Promise<ZoneItemNormalized[]> {
4817
- if(!data.countryCode?.length || !data.level?.length){
4876
+ if (!data.countryCode?.length || !data.level?.length) {
4818
4877
  throw new ApiError("countryCode et level sont requis.", 400);
4819
4878
  }
4820
4879
  const wrappedFinalizer = this._withCostumContext(
@@ -4854,10 +4913,286 @@ export class BaseEntity<TServerData = any> {
4854
4913
  if (!formId || typeof formId !== "string") {
4855
4914
  throw new ApiError("formId est requis et doit être une chaîne de caractères.", 400);
4856
4915
  }
4857
- const result = await this.endpointApi.generateAnswerFromForm({pathParams: { formId }, action: "new" });
4916
+ const result = await this.endpointApi.generateAnswerFromForm({ pathParams: { formId }, action: "new" });
4858
4917
  return this._linkEntity?.(result.collection, result) ?? result;
4859
4918
  }
4860
4919
 
4920
+ /**
4921
+ * Associe un compte Discourse à l'utilisateur courant dans le contexte de l'instance.
4922
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
4923
+ *
4924
+ * @param username - Nom d'utilisateur Discourse à associer
4925
+ * @returns Résultat de la liaison avec les informations du compte Discourse trouvé
4926
+ * @throws {ApiError} Si `username` est absent ou invalide
4927
+ *
4928
+ * @example
4929
+ * const result = await user.linkDiscourseAccount("john_doe");
4930
+ * if (result.result) {
4931
+ * console.log(result.profileUrl); // URL du profil Discourse
4932
+ * } else {
4933
+ * console.error(result.error);
4934
+ * }
4935
+ */
4936
+ async linkDiscourseAccount(
4937
+ username: string,
4938
+ ): Promise<{
4939
+ result: boolean,
4940
+ error?: string
4941
+ username?: string
4942
+ profileUrl?: string
4943
+ }> {
4944
+ if (!username || typeof username !== "string") {
4945
+ throw new ApiError("username est requis et doit être une chaîne de caractères.", 400);
4946
+ }
4947
+ const wrappedFinalizer = this._withCostumContext(
4948
+ (finalData: WithCostumContext<{ username: string }>) =>
4949
+ this.endpointApi.linkDiscourseAccount({
4950
+ ...finalData,
4951
+ costumId: finalData.contextId,
4952
+ costumType: finalData.contextType,
4953
+ } as LinkDiscourseAccountData)
4954
+ );
4955
+ return wrappedFinalizer({ username });
4956
+ }
4957
+
4958
+ /**
4959
+ * Dissocie le compte Discourse de l'utilisateur courant dans le contexte de l'instance.
4960
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
4961
+ *
4962
+ * @returns Résultat du délien du compte Discourse
4963
+ *
4964
+ * @example
4965
+ * const result = await user.unlinkDiscourseAccount();
4966
+ * result.result; // true si succès
4967
+ */
4968
+ async unlinkDiscourseAccount(): Promise<{
4969
+ result: boolean;
4970
+ error?: string;
4971
+ }> {
4972
+ const wrappedFinalizer = this._withCostumContext(
4973
+ (finalData: WithCostumContext<Record<string, never>>) =>
4974
+ this.endpointApi.unlinkDiscourseAccount({
4975
+ ...finalData,
4976
+ costumId: finalData.contextId,
4977
+ costumType: finalData.contextType,
4978
+ } as UnlinkDiscourseAccountData)
4979
+ );
4980
+ return wrappedFinalizer({});
4981
+ }
4982
+
4983
+ /**
4984
+ * Récupère le profil Discourse d'un utilisateur dans le contexte de l'instance.
4985
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
4986
+ *
4987
+ * @param username - Nom d'utilisateur Discourse cible
4988
+ * @returns Profil Discourse avec `summary` et `profileUrl`, ou `error` en cas d'échec
4989
+ * @throws {ApiError} Si `username` est absent ou invalide
4990
+ *
4991
+ * @example
4992
+ * const result = await user.getDiscourseProfile("john_doe");
4993
+ * result.profileUrl; // "https://forum.example.org/u/john_doe/summary"
4994
+ */
4995
+ async getDiscourseProfile(
4996
+ username: string,
4997
+ ): Promise<{
4998
+ summary?: Record<string, unknown>;
4999
+ profileUrl?: string;
5000
+ error?: string;
5001
+ }> {
5002
+ if (!username || typeof username !== "string") {
5003
+ throw new ApiError("username est requis et doit être une chaîne de caractères.", 400);
5004
+ }
5005
+ const wrappedFinalizer = this._withCostumContext(
5006
+ (finalData: WithCostumContext<{ username: string }>) =>
5007
+ this.endpointApi.discourseProfile({
5008
+ ...finalData,
5009
+ costumId: finalData.contextId,
5010
+ costumType: finalData.contextType,
5011
+ } as DiscourseProfileData)
5012
+ );
5013
+ return wrappedFinalizer({ username });
5014
+ }
5015
+
5016
+ /**
5017
+ * Vérifie si l'email de l'utilisateur connecté correspond à un compte Discourse.
5018
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
5019
+ * L'email utilisé est celui de l'utilisateur connecté (géré côté serveur).
5020
+ *
5021
+ * @returns `{ found: true, user }` si un compte correspondant existe, `{ found: false }` sinon
5022
+ *
5023
+ * @example
5024
+ * const result = await user.checkDiscourseEmailMatch();
5025
+ * if (result.found) {
5026
+ * console.log(result.user); // infos du compte Discourse trouvé
5027
+ * }
5028
+ */
5029
+ async checkDiscourseEmailMatch(): Promise<{
5030
+ found: boolean;
5031
+ user?: Record<string, unknown>;
5032
+ }> {
5033
+ const wrappedFinalizer = this._withCostumContext(
5034
+ (finalData: WithCostumContext<Record<string, never>>) =>
5035
+ this.endpointApi.discourseCheckEmail({
5036
+ ...finalData,
5037
+ costumId: finalData.contextId,
5038
+ costumType: finalData.contextType,
5039
+ } as DiscourseCheckEmailData)
5040
+ );
5041
+ return wrappedFinalizer({});
5042
+ }
5043
+
5044
+ /**
5045
+ * Ignore la suggestion de liaison de compte Discourse pour l'instance courante.
5046
+ * Persiste le refus côté serveur (`interop.discourse[costumSlug] = false`).
5047
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
5048
+ *
5049
+ * @returns `{ result: true }` si le dismiss a réussi
5050
+ *
5051
+ * @example
5052
+ * const result = await user.dismissDiscourseLink();
5053
+ * result.result; // true
5054
+ */
5055
+ async dismissDiscourseLink(): Promise<{
5056
+ result: boolean;
5057
+ error?: string;
5058
+ }> {
5059
+ const wrappedFinalizer = this._withCostumContext(
5060
+ (finalData: WithCostumContext<Record<string, never>>) =>
5061
+ this.endpointApi.discourseDismissLink({
5062
+ ...finalData,
5063
+ costumId: finalData.contextId,
5064
+ costumType: finalData.contextType,
5065
+ } as DiscourseDismissLinkData)
5066
+ );
5067
+ return wrappedFinalizer({});
5068
+ }
5069
+
5070
+ /**
5071
+ * Associe un compte MediaWiki à l'entité courante dans le contexte de l'instance.
5072
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
5073
+ *
5074
+ * @param username - Nom d'utilisateur MediaWiki à associer
5075
+ * @returns Résultat de la liaison avec les informations du compte MediaWiki trouvé
5076
+ * @throws {ApiError} Si `username` est absent ou invalide
5077
+ *
5078
+ * @example
5079
+ * const result = await user.linkMediaWikiAccount("John_Doe");
5080
+ * if (result.result) {
5081
+ * console.log(result.username); // nom d'utilisateur confirmé
5082
+ * } else {
5083
+ * console.error(result.error);
5084
+ * }
5085
+ */
5086
+ async linkMediaWikiAccount(
5087
+ username: string,
5088
+ ): Promise<{
5089
+ result: boolean;
5090
+ error?: string;
5091
+ username?: string;
5092
+ msg?: string;
5093
+ }> {
5094
+ if (!username || typeof username !== "string") {
5095
+ throw new ApiError("username est requis et doit être une chaîne de caractères.", 400);
5096
+ }
5097
+ const wrappedFinalizer = this._withCostumContext(
5098
+ (finalData: WithCostumContext<{ username: string }>) =>
5099
+ this.endpointApi.linkMediawikiAccount({
5100
+ ...finalData,
5101
+ costumId: finalData.contextId,
5102
+ costumType: finalData.contextType,
5103
+ } as LinkMediawikiAccountData)
5104
+ );
5105
+ return wrappedFinalizer({ username });
5106
+ }
5107
+
5108
+ /**
5109
+ * Dissocie le compte MediaWiki de l'entité courante dans le contexte de l'instance.
5110
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
5111
+ *
5112
+ * @returns Résultat du délien du compte MediaWiki
5113
+ *
5114
+ * @example
5115
+ * const result = await user.unlinkMediaWikiAccount();
5116
+ * result.result; // true si succès
5117
+ */
5118
+ async unlinkMediaWikiAccount(): Promise<{
5119
+ result: boolean;
5120
+ error?: string;
5121
+ msg?: string;
5122
+ }> {
5123
+ const wrappedFinalizer = this._withCostumContext(
5124
+ (finalData: WithCostumContext<Record<string, never>>) =>
5125
+ this.endpointApi.unlinkMediawikiAccount({
5126
+ ...finalData,
5127
+ costumId: finalData.contextId,
5128
+ costumType: finalData.contextType,
5129
+ } as UnlinkMediawikiAccountData)
5130
+ );
5131
+ return wrappedFinalizer({});
5132
+ }
5133
+
5134
+ /**
5135
+ * Récupère les contributions MediaWiki d'un utilisateur dans le contexte de l'instance.
5136
+ * Injecte automatiquement le `costumSlug` via `_withCostumContext`.
5137
+ *
5138
+ * @param username - Nom d'utilisateur MediaWiki cible
5139
+ * @param limit - Nombre maximum de contributions à retourner (optionnel)
5140
+ * @returns Liste des contributions MediaWiki, ou `error` en cas d'échec
5141
+ * @throws {ApiError} Si `username` est absent ou invalide
5142
+ *
5143
+ * @example
5144
+ * const result = await user.getMediaWikiContributions("John_Doe", 10);
5145
+ * result.contribs; // objet contenant les contributions
5146
+ */
5147
+ async getMediaWikiContributions(
5148
+ username: string,
5149
+ limit?: number,
5150
+ ): Promise<{
5151
+ result: boolean;
5152
+ contribs?: Record<string, unknown>;
5153
+ }> {
5154
+ if (!username || typeof username !== "string") {
5155
+ throw new ApiError("username est requis et doit être une chaîne de caractères.", 400);
5156
+ }
5157
+ const wrappedFinalizer = this._withCostumContext(
5158
+ (finalData: WithCostumContext<{ username: string; limit?: number }>) =>
5159
+ this.endpointApi.getMediawikiContributions({
5160
+ ...finalData,
5161
+ costumId: finalData.contextId,
5162
+ costumType: finalData.contextType,
5163
+ } as GetMediawikiContributionsData)
5164
+ );
5165
+ return wrappedFinalizer({ username, limit });
5166
+ }
5167
+
5168
+ /**
5169
+ * Wrapper pour l'endpoint COREMU_OPERATION : génère une proposition à partir
5170
+ * d'un formulaire et d'un projet. Cette méthode injecte les `const` du schéma
5171
+ * (`answer: "new"`, `action: "generateproposition"`) et délègue à l'EndpointApi.
5172
+ *
5173
+ * @param data.form - ID Mongo (24 hex) du formulaire
5174
+ * @param data.project - ID Mongo (24 hex) du projet
5175
+ * @returns Résultat brut de l'API (ou entité liée si `collection` est présent)
5176
+ * @throws {ApiError} Si les paramètres sont manquants ou invalides
5177
+ */
5178
+ async coremuOperation(data: Pick<CoremuOperationData, "form" | "project">): Promise<any> {
5179
+ if (!data.form || typeof data.form !== "string") {
5180
+ throw new ApiError("form est requis et doit être une chaîne de caractères.", 400);
5181
+ }
5182
+ if (!data.project || typeof data.project !== "string") {
5183
+ throw new ApiError("project est requis et doit être une chaîne de caractères.", 400);
5184
+ }
5185
+
5186
+ const payload: CoremuOperationData = {
5187
+ ...data,
5188
+ answer: "new",
5189
+ action: "generateproposition",
5190
+ };
5191
+
5192
+ const result = await this.endpointApi.coremuOperation(payload);
5193
+ return this._linkEntity(result?.collection, result) ?? result;
5194
+ }
5195
+
4861
5196
  /**
4862
5197
  * ───────────────────────────────
4863
5198
  * Pagination restoration methods