@communecter/cocolight-api-client 1.0.54 → 1.0.56
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/401.cocolight-api-client.browser.js +1 -0
- package/dist/401.cocolight-api-client.cjs +1 -0
- package/dist/401.cocolight-api-client.mjs.js +1 -0
- package/dist/588.cocolight-api-client.browser.js +1 -0
- package/dist/588.cocolight-api-client.cjs +1 -0
- package/dist/588.cocolight-api-client.mjs.js +1 -0
- package/dist/593.cocolight-api-client.browser.js +1 -0
- package/dist/593.cocolight-api-client.cjs +1 -0
- package/dist/593.cocolight-api-client.mjs.js +1 -0
- package/dist/839.cocolight-api-client.browser.js +1 -0
- package/dist/839.cocolight-api-client.cjs +1 -0
- package/dist/839.cocolight-api-client.mjs.js +1 -0
- package/dist/cocolight-api-client.browser.js +3 -3
- 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 +29 -17
- package/src/{Api.js → Api.ts} +85 -95
- package/src/{ApiClient.js → ApiClient.ts} +436 -247
- package/src/EJSONType.ts +103 -0
- package/src/api/{Badge.js → Badge.ts} +56 -45
- package/src/api/BaseEntity.ts +3890 -0
- package/src/api/Comment.ts +200 -0
- package/src/api/{EndpointApi.js → EndpointApi.ts} +363 -297
- package/src/api/EndpointApi.types.ts +4609 -0
- package/src/api/EntityRegistry.ts +203 -0
- package/src/api/Event.ts +332 -0
- package/src/api/News.ts +331 -0
- package/src/api/{Organization.js → Organization.ts} +155 -119
- package/src/api/{Poi.js → Poi.ts} +68 -60
- package/src/api/{Project.js → Project.ts} +150 -127
- package/src/api/{User.js → User.ts} +321 -256
- package/src/api/UserApi.ts +148 -0
- package/src/api/serverDataType/Comment.ts +88 -0
- package/src/api/serverDataType/Event.ts +80 -0
- package/src/api/serverDataType/News.ts +138 -0
- package/src/api/serverDataType/Organization.ts +80 -0
- package/src/api/serverDataType/Project.ts +71 -0
- package/src/api/serverDataType/User.ts +103 -0
- package/src/api/serverDataType/common.ts +80 -0
- package/src/endpoints.module.ts +2621 -0
- package/src/error.ts +86 -0
- package/src/index.ts +86 -0
- package/src/mixin/UserMixin.ts +4 -0
- package/src/types/api-responses.ts +217 -0
- package/src/types/entities.ts +22 -0
- package/src/types/error-guards.ts +230 -0
- package/src/types/index.ts +39 -0
- package/src/types/payloads.ts +21 -0
- package/src/types/transforms.ts +110 -0
- package/src/utils/{FileOfflineStorageStrategy.node.js → FileOfflineStorageStrategy.node.ts} +15 -12
- package/src/utils/{FileStorageStrategy.node.js → FileStorageStrategy.node.ts} +16 -39
- package/src/utils/MultiServerFileStorageStrategy.node.ts +67 -0
- package/src/utils/MultiServerTokenStorageStrategy.ts +139 -0
- package/src/utils/{OfflineClientManager.js → OfflineClientManager.ts} +82 -86
- package/src/utils/OfflineQueueStorageStrategy.ts +47 -0
- package/src/utils/TokenStorage.ts +77 -0
- package/src/utils/compat.ts +12 -0
- package/src/utils/createDefaultMultiServerTokenStorageStrategy.ts +35 -0
- package/src/utils/{createDefaultOfflineStrategy.js → createDefaultOfflineStrategy.ts} +8 -3
- package/src/utils/createDefaultTokenStorageStrategy.ts +33 -0
- package/src/utils/{reactive.js → reactive.ts} +49 -40
- package/src/utils/stream-utils.node.ts +12 -0
- package/types/Api.d.ts +38 -82
- package/types/Api.d.ts.map +1 -0
- package/types/ApiClient.d.ts +244 -184
- package/types/ApiClient.d.ts.map +1 -0
- package/types/EJSONType.d.ts +48 -22
- package/types/EJSONType.d.ts.map +1 -0
- package/types/api/Badge.d.ts +20 -20
- package/types/api/Badge.d.ts.map +1 -0
- package/types/api/BaseEntity.d.ts +751 -446
- package/types/api/BaseEntity.d.ts.map +1 -0
- package/types/api/Comment.d.ts +36 -0
- package/types/api/EndpointApi.d.ts +347 -295
- package/types/api/EndpointApi.d.ts.map +1 -0
- package/types/api/EndpointApi.types.d.ts +3914 -4133
- package/types/api/EntityRegistry.d.ts +18 -16
- package/types/api/EntityRegistry.d.ts.map +1 -0
- package/types/api/Event.d.ts +119 -35
- package/types/api/Event.d.ts.map +1 -0
- package/types/api/News.d.ts +52 -20
- package/types/api/News.d.ts.map +1 -0
- package/types/api/Organization.d.ts +165 -49
- package/types/api/Organization.d.ts.map +1 -0
- package/types/api/Poi.d.ts +51 -22
- package/types/api/Poi.d.ts.map +1 -0
- package/types/api/Project.d.ts +151 -52
- package/types/api/Project.d.ts.map +1 -0
- package/types/api/User.d.ts +222 -93
- package/types/api/User.d.ts.map +1 -0
- package/types/api/UserApi.d.ts +60 -9
- package/types/api/UserApi.d.ts.map +1 -0
- package/types/api/serverDataType/Comment.d.ts +83 -0
- package/types/api/serverDataType/Event.d.ts +67 -0
- package/types/api/serverDataType/News.d.ts +130 -0
- package/types/api/serverDataType/Organization.d.ts +65 -0
- package/types/api/serverDataType/Organization.d.ts.map +1 -0
- package/types/api/serverDataType/Project.d.ts +58 -0
- package/types/api/serverDataType/Project.d.ts.map +1 -0
- package/types/api/serverDataType/User.d.ts +86 -0
- package/types/api/serverDataType/User.d.ts.map +1 -0
- package/types/api/serverDataType/common.d.ts +71 -0
- package/types/api/serverDataType/common.d.ts.map +1 -0
- package/types/endpoints.module.d.ts +6922 -1215
- package/types/endpoints.module.d.ts.map +1 -0
- package/types/error.d.ts +25 -51
- package/types/error.d.ts.map +1 -0
- package/types/index.d.ts +55 -48
- package/types/index.d.ts.map +1 -0
- package/types/mixin/UserMixin.d.ts +1 -1
- package/types/mixin/UserMixin.d.ts.map +1 -0
- package/types/types/api-responses.d.ts +190 -0
- package/types/types/api-responses.d.ts.map +1 -0
- package/types/types/entities.d.ts +17 -0
- package/types/types/entities.d.ts.map +1 -0
- package/types/types/error-guards.d.ts +99 -0
- package/types/types/error-guards.d.ts.map +1 -0
- package/types/types/index.d.ts +7 -0
- package/types/types/payloads.d.ts +17 -0
- package/types/types/payloads.d.ts.map +1 -0
- package/types/types/transforms.d.ts +79 -0
- package/types/types/transforms.d.ts.map +1 -0
- package/types/utils/FileOfflineStorageStrategy.node.d.ts +10 -9
- package/types/utils/FileOfflineStorageStrategy.node.d.ts.map +1 -0
- package/types/utils/FileStorageStrategy.node.d.ts +9 -20
- package/types/utils/FileStorageStrategy.node.d.ts.map +1 -0
- package/types/utils/MultiServerFileStorageStrategy.node.d.ts +13 -18
- package/types/utils/MultiServerFileStorageStrategy.node.d.ts.map +1 -0
- package/types/utils/MultiServerTokenStorageStrategy.d.ts +30 -51
- package/types/utils/MultiServerTokenStorageStrategy.d.ts.map +1 -0
- package/types/utils/OfflineClientManager.d.ts +52 -88
- package/types/utils/OfflineClientManager.d.ts.map +1 -0
- package/types/utils/OfflineQueueStorageStrategy.d.ts +12 -9
- package/types/utils/OfflineQueueStorageStrategy.d.ts.map +1 -0
- package/types/utils/TokenStorage.d.ts +20 -70
- package/types/utils/TokenStorage.d.ts.map +1 -0
- package/types/utils/compat.d.ts +4 -0
- package/types/utils/compat.d.ts.map +1 -0
- package/types/utils/createDefaultMultiServerTokenStorageStrategy.d.ts +2 -11
- package/types/utils/createDefaultMultiServerTokenStorageStrategy.d.ts.map +1 -0
- package/types/utils/createDefaultOfflineStrategy.d.ts +2 -3
- package/types/utils/createDefaultOfflineStrategy.d.ts.map +1 -0
- package/types/utils/createDefaultTokenStorageStrategy.d.ts +2 -12
- package/types/utils/createDefaultTokenStorageStrategy.d.ts.map +1 -0
- package/types/utils/reactive.d.ts +10 -16
- package/types/utils/reactive.d.ts.map +1 -0
- package/types/utils/stream-utils.node.d.ts +3 -2
- package/types/utils/stream-utils.node.d.ts.map +1 -0
- package/dist/123.cocolight-api-client.browser.js +0 -1
- package/dist/123.cocolight-api-client.cjs +0 -1
- package/dist/22.cocolight-api-client.mjs.js +0 -1
- package/dist/339.cocolight-api-client.mjs.js +0 -1
- package/dist/394.cocolight-api-client.browser.js +0 -1
- package/dist/394.cocolight-api-client.cjs +0 -1
- package/dist/405.cocolight-api-client.browser.js +0 -1
- package/dist/405.cocolight-api-client.cjs +0 -1
- package/dist/774.cocolight-api-client.mjs.js +0 -1
- package/dist/790.cocolight-api-client.mjs.js +0 -1
- package/dist/931.cocolight-api-client.browser.js +0 -1
- package/dist/931.cocolight-api-client.cjs +0 -1
- package/src/EJSONType.js +0 -53
- package/src/api/BaseEntity.js +0 -2828
- package/src/api/EntityRegistry.js +0 -152
- package/src/api/Event.js +0 -226
- package/src/api/News.js +0 -244
- package/src/api/UserApi.js +0 -81
- package/src/endpoints.module.js +0 -5
- package/src/error.js +0 -121
- package/src/index.js +0 -97
- package/src/mixin/UserMixin.js +0 -8
- package/src/utils/MultiServerFileStorageStrategy.node.js +0 -87
- package/src/utils/MultiServerTokenStorageStrategy.js +0 -188
- package/src/utils/OfflineQueueStorageStrategy.js +0 -51
- package/src/utils/TokenStorage.js +0 -153
- package/src/utils/createDefaultMultiServerTokenStorageStrategy.js +0 -51
- package/src/utils/createDefaultTokenStorageStrategy.js +0 -49
- package/src/utils/stream-utils.node.js +0 -10
|
@@ -0,0 +1,3890 @@
|
|
|
1
|
+
// BaseEntity.ts
|
|
2
|
+
import objectId from "bson-objectid";
|
|
3
|
+
// import { fileTypeFromBuffer } from "file-type";
|
|
4
|
+
import * as pkg from "file-type";
|
|
5
|
+
|
|
6
|
+
import { ApiAuthenticationError, ApiError, ApiResponseError, ApiValidationError } from "../error.js";
|
|
7
|
+
import { EJSON } from "../utils/compat.js";
|
|
8
|
+
import { isReactive, isSignal, reactive } from "../utils/reactive.js";
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ConnectData,
|
|
12
|
+
DisconnectData,
|
|
13
|
+
LinkValidateData,
|
|
14
|
+
FollowData,
|
|
15
|
+
UpdateSettingsData,
|
|
16
|
+
UpdateBlockDescriptionData,
|
|
17
|
+
UpdateBlockInfoData,
|
|
18
|
+
UpdateBlockSocialData,
|
|
19
|
+
UpdateBlockLocalityData,
|
|
20
|
+
UpdateBlockSlugData,
|
|
21
|
+
CheckData,
|
|
22
|
+
ProfilImageData,
|
|
23
|
+
GetNewsData,
|
|
24
|
+
GetGalleryData,
|
|
25
|
+
GetCostumJsonData,
|
|
26
|
+
GlobalAutocompleteCostumData,
|
|
27
|
+
CostumEventRequestActorsData,
|
|
28
|
+
CostumEventRequestSubeventsData,
|
|
29
|
+
CostumEventRequestDatesData,
|
|
30
|
+
CostumEventRequestElementEventData,
|
|
31
|
+
CostumEventRequestCategoriesData,
|
|
32
|
+
CostumEventRequestEventData,
|
|
33
|
+
CostumEventRequestLinkTlToEventData,
|
|
34
|
+
CostumEventRequestLoadContextTagData,
|
|
35
|
+
GetOrganizationsAdminData,
|
|
36
|
+
GetOrganizationsNoAdminData,
|
|
37
|
+
GetProjectsAdminData,
|
|
38
|
+
GetProjectsNoAdminData,
|
|
39
|
+
GetPoisAdminData,
|
|
40
|
+
GetPoisNoAdminData,
|
|
41
|
+
GetSubscribersData,
|
|
42
|
+
GetBadgesData
|
|
43
|
+
} from "./EndpointApi.types.js";
|
|
44
|
+
import type { GetElementsKeyResponse } from "../types/api-responses.js";
|
|
45
|
+
import type { TransformsMap } from "../types/entities.js";
|
|
46
|
+
const { fromBuffer } = pkg;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Types pour les méthodes d'entité (organization, project, poi, event, badge, news)
|
|
50
|
+
* Permettent soit de récupérer une entité existante (GET), soit de créer une nouvelle instance (CREATE)
|
|
51
|
+
*/
|
|
52
|
+
type OrganizationInput = { id: string } | { slug: string } | Record<string, any>;
|
|
53
|
+
type ProjectInput = { id: string } | { slug: string } | Record<string, any>;
|
|
54
|
+
type PoiInput = { id: string } | { slug: string } | Record<string, any>;
|
|
55
|
+
type EventInput = { id: string } | { slug: string } | Record<string, any>;
|
|
56
|
+
type BadgeInput = { id: string } | Record<string, any>;
|
|
57
|
+
type NewsInput = { id: string } | Record<string, any>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* On force le type de l'import comme une fabrique qui renvoie un objet
|
|
61
|
+
* possédant au moins .toHexString().
|
|
62
|
+
* (peu importe la d.ts du module, on impose notre contrat minimal)
|
|
63
|
+
*/
|
|
64
|
+
const createObjectId: (arg?: number|string|number[]|Buffer) => { toHexString(): string } = objectId as any;
|
|
65
|
+
|
|
66
|
+
// Types TypeScript importés
|
|
67
|
+
type ApiClient = import("../ApiClient.js").default;
|
|
68
|
+
type EndpointApi = import("./EndpointApi.js").default;
|
|
69
|
+
type User = import("./User.js").User;
|
|
70
|
+
type Organization = import("./Organization.js").Organization;
|
|
71
|
+
type Project = import("./Project.js").Project;
|
|
72
|
+
type EventEntity = import("./Event.js").Event;
|
|
73
|
+
type Poi = import("./Poi.js").Poi;
|
|
74
|
+
type News = import("./News.js").News;
|
|
75
|
+
type Badge = import("./Badge.js").Badge;
|
|
76
|
+
type Comment = import("./Comment.js").Comment;
|
|
77
|
+
|
|
78
|
+
// Types d'union
|
|
79
|
+
type AnyEntity = User | Organization | Project | Poi | EventEntity | Badge | News | Comment;
|
|
80
|
+
type ParentLike = BaseEntity<any> & { apiClient: ApiClient, userContext?: User | null };
|
|
81
|
+
|
|
82
|
+
// Types pour les dépendances
|
|
83
|
+
type EndpointApiCtor = { new(apiClient: ApiClient): EndpointApi };
|
|
84
|
+
type EndpointApiDep = EndpointApi | EndpointApiCtor;
|
|
85
|
+
|
|
86
|
+
interface Deps {
|
|
87
|
+
EndpointApi: EndpointApiDep;
|
|
88
|
+
User?: any;
|
|
89
|
+
Organization?: any;
|
|
90
|
+
Project?: any;
|
|
91
|
+
Poi?: any;
|
|
92
|
+
Event?: any;
|
|
93
|
+
Badge?: any;
|
|
94
|
+
News?: any;
|
|
95
|
+
Comment?: any;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface BaseEntityConfig {
|
|
99
|
+
entityTag?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// EntityTypeMap - mappe les clés "collection" vers leurs classes d'entités
|
|
103
|
+
interface EntityTypeMap {
|
|
104
|
+
citoyens: User;
|
|
105
|
+
organizations: Organization;
|
|
106
|
+
projects: Project;
|
|
107
|
+
events: EventEntity;
|
|
108
|
+
poi: Poi;
|
|
109
|
+
news: News;
|
|
110
|
+
badges: Badge;
|
|
111
|
+
comments: Comment;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Types pour les streams et uploads
|
|
115
|
+
type ReadableWithMeta = import("stream").Readable & { path?: string, mimeType?: string };
|
|
116
|
+
type UploadInput = File | Blob | Buffer | import("stream").Readable;
|
|
117
|
+
type ValidatedUpload = File | Buffer | ReadableWithMeta;
|
|
118
|
+
|
|
119
|
+
// PaginatorPage interface pour les résultats paginés
|
|
120
|
+
export interface PaginatorPage<T> {
|
|
121
|
+
count: {
|
|
122
|
+
total: number;
|
|
123
|
+
};
|
|
124
|
+
results: T[];
|
|
125
|
+
pageIndex: number;
|
|
126
|
+
pageNumber: number;
|
|
127
|
+
hasNext: boolean;
|
|
128
|
+
hasPrev: boolean;
|
|
129
|
+
next?: () => Promise<PaginatorPage<T>>;
|
|
130
|
+
prev?: () => Promise<PaginatorPage<T>>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Type helper pour extraire les noms de méthodes depuis un Map as const
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* static UPDATE_BLOCKS = new Map([
|
|
138
|
+
* ["UPDATE_BLOCK_DESCRIPTION", "updateDescription"],
|
|
139
|
+
* ["UPDATE_BLOCK_INFO", "updateInfo"]
|
|
140
|
+
* ] as const);
|
|
141
|
+
*
|
|
142
|
+
* type MethodNames = ExtractMethodNames<typeof MyClass.UPDATE_BLOCKS>;
|
|
143
|
+
* // Result: "updateDescription" | "updateInfo"
|
|
144
|
+
*/
|
|
145
|
+
export type ExtractMethodNames<T> = T extends Map<any, infer V>
|
|
146
|
+
? V extends readonly (readonly [any, infer M])[]
|
|
147
|
+
? M
|
|
148
|
+
: never
|
|
149
|
+
: never;
|
|
150
|
+
|
|
151
|
+
// LinkMeta interface
|
|
152
|
+
interface LinkMeta {
|
|
153
|
+
linkType: "memberOf" | "projects" | "events" | "friends";
|
|
154
|
+
connectTypeConnect: "member" | "contributor" | "attendee" | "friend";
|
|
155
|
+
connectTypeDisconnect: "members" | "contributors" | "attendees" | "friends";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Type du constructeur de BaseEntity avec ses propriétés statiques
|
|
159
|
+
type BaseEntityCtor = typeof BaseEntity & {
|
|
160
|
+
entityTag: string;
|
|
161
|
+
entityType: "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments";
|
|
162
|
+
SCHEMA_CONSTANTS: string | string[];
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Constructeur d'entité compatible (new + fromServerData)
|
|
166
|
+
type EntityCtorLike = typeof BaseEntity & {
|
|
167
|
+
new(parent: ApiClient | ParentLike, data?: object, deps?: Deps, config?: BaseEntityConfig): AnyEntity;
|
|
168
|
+
fromServerData(data: any, parent: ApiClient | ParentLike, deps: Deps): AnyEntity;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Métadonnées retournées par _getEntityMeta
|
|
172
|
+
type EntityMeta = { entityClass: EntityCtorLike; deps: Deps };
|
|
173
|
+
|
|
174
|
+
// Types de filtres pour les requêtes MongoDB
|
|
175
|
+
interface FilterValueExistsOnly {
|
|
176
|
+
$exists: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface FilterValueInOnly {
|
|
180
|
+
$in: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Type pour JSON Schema avec support des mots-clés standards
|
|
184
|
+
type JsonSchema = Record<string, unknown> & {
|
|
185
|
+
$id?: string;
|
|
186
|
+
$ref?: string;
|
|
187
|
+
$defs?: Record<string, JsonSchema>;
|
|
188
|
+
definitions?: Record<string, JsonSchema>;
|
|
189
|
+
allOf?: JsonSchema[];
|
|
190
|
+
if?: JsonSchema;
|
|
191
|
+
then?: JsonSchema;
|
|
192
|
+
else?: JsonSchema;
|
|
193
|
+
properties?: Record<string, JsonSchemaProperty>;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Type pour les propriétés de schéma JSON
|
|
197
|
+
type JsonSchemaProperty = Record<string, unknown> & {
|
|
198
|
+
readOnly?: boolean;
|
|
199
|
+
const?: unknown;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// Type pour le curseur de pagination utilisé dans _createPaginatorEngine
|
|
203
|
+
type PaginationCursor = {
|
|
204
|
+
searchType?: string[];
|
|
205
|
+
searchBy?: string;
|
|
206
|
+
countType?: string[];
|
|
207
|
+
ranges?: Record<string, { indexMin: number; indexMax: number }>;
|
|
208
|
+
indexMin?: number;
|
|
209
|
+
indexMax?: number;
|
|
210
|
+
indexStep?: number;
|
|
211
|
+
[key: string]: unknown; // Pour les propriétés additionnelles dynamiques
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
interface FilterValueCombined {
|
|
215
|
+
$in: string[];
|
|
216
|
+
$exists: boolean;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type FilterValue = FilterValueExistsOnly | FilterValueInOnly | FilterValueCombined;
|
|
220
|
+
|
|
221
|
+
type LinkFilters = Record<string, FilterValue>;
|
|
222
|
+
|
|
223
|
+
// Type pour la validation de lien utilisateur
|
|
224
|
+
interface MinimalUserLink {
|
|
225
|
+
toBeValidated?: boolean;
|
|
226
|
+
isInviting?: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Types pour linkEntitiesFromServerData
|
|
230
|
+
type EntityFilterValue = string | boolean | RegExp | ((value: any) => boolean);
|
|
231
|
+
type EntityFilters = Record<string, EntityFilterValue>;
|
|
232
|
+
|
|
233
|
+
interface LinkEntitiesOptions {
|
|
234
|
+
key?: string;
|
|
235
|
+
mapFn?: (entity: any) => any;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================================
|
|
239
|
+
// Types convertis depuis JSDoc @typedef
|
|
240
|
+
// ============================================================================
|
|
241
|
+
|
|
242
|
+
// Type pour les entités supprimables
|
|
243
|
+
type Deletable = { _isDeleted?: boolean };
|
|
244
|
+
|
|
245
|
+
// Type pour les données réactives (objets qui peuvent être wrappés avec reactive())
|
|
246
|
+
// Les objets réactifs ont une propriété __isReactive cachée
|
|
247
|
+
type ReactiveData = Record<string, any> & { __isReactive?: boolean; __raw?: any };
|
|
248
|
+
|
|
249
|
+
// Types pour les adresses et géolocalisation
|
|
250
|
+
type Address = UpdateBlockLocalityData["address"];
|
|
251
|
+
type PostalAddress = Exclude<Address, "">;
|
|
252
|
+
type AddressGeo = UpdateBlockLocalityData["geo"];
|
|
253
|
+
type Geo = Exclude<AddressGeo, "">;
|
|
254
|
+
type AddressGeoPosition = UpdateBlockLocalityData["geoPosition"];
|
|
255
|
+
type GeoPosition = Exclude<AddressGeoPosition, "">;
|
|
256
|
+
|
|
257
|
+
// Types pour le système de pagination
|
|
258
|
+
interface FinalizerResult<TOut> {
|
|
259
|
+
results: TOut[];
|
|
260
|
+
count: { total: number } & Record<string, any>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Types pour les entités
|
|
264
|
+
type EntityType = "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news";
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
/** Détecte un Readable Node sans toucher à des props privées */
|
|
269
|
+
function isNodeReadable(obj: any) {
|
|
270
|
+
return !!obj && typeof obj === "object"
|
|
271
|
+
&& typeof (obj as any).pipe === "function"
|
|
272
|
+
&& typeof (obj as any).on === "function";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Classe de base pour toutes les entités métiers : utilisateurs, projets, organisations, etc.
|
|
277
|
+
* Fournit un système de brouillon (draft), transformation, appel API sécurisé,
|
|
278
|
+
* et gestion de données côté client avec support du mode offline.
|
|
279
|
+
* @template object TServerData
|
|
280
|
+
* @abstract
|
|
281
|
+
*/
|
|
282
|
+
export class BaseEntity<TServerData = any> {
|
|
283
|
+
// Propriétés TypeScript avec types corrects
|
|
284
|
+
__entityTag: string;
|
|
285
|
+
deps: Partial<Deps>;
|
|
286
|
+
apiClient: ApiClient;
|
|
287
|
+
parent: ParentLike | null;
|
|
288
|
+
userContext: User | null;
|
|
289
|
+
endpointApi: EndpointApi;
|
|
290
|
+
data: any; // Proxy dynamique - difficile à typer précisément
|
|
291
|
+
meta?: any; // Métadonnées ajoutées dynamiquement par linkEntitiesFromServerData()
|
|
292
|
+
|
|
293
|
+
// Propriétés existantes avec types TypeScript
|
|
294
|
+
// _draftData est réactif (wrappé avec reactive())
|
|
295
|
+
_draftData: ReactiveData = {};
|
|
296
|
+
// _initialDraftData est un snapshot non-réactif (plain object)
|
|
297
|
+
_initialDraftData: Record<string, any> = {};
|
|
298
|
+
// _serverData est réactif (wrappé avec reactive()) et typé selon TServerData
|
|
299
|
+
_serverData: TServerData & ReactiveData = reactive({}) as TServerData & ReactiveData;
|
|
300
|
+
_calledFromSave: boolean = false;
|
|
301
|
+
_syncReactiveDraft: boolean = false;
|
|
302
|
+
_isDeleted: boolean = false;
|
|
303
|
+
_add?: ((payload: any) => Promise<void | unknown>) | undefined;
|
|
304
|
+
_update?: ((payload: any) => Promise<boolean>) | undefined;
|
|
305
|
+
|
|
306
|
+
// Metadata pour recalcul dynamique des allowedFields dans le proxy
|
|
307
|
+
_allowedFieldsCache?: string[];
|
|
308
|
+
_allowedFieldsMetadata?: {
|
|
309
|
+
combinedSchema: any;
|
|
310
|
+
transforms: TransformsMap;
|
|
311
|
+
removeFields: string[];
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
static entityTag = "BaseEntity";
|
|
315
|
+
static entityType?: string;
|
|
316
|
+
static SCHEMA_CONSTANTS?: string | string[];
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Vérifie que l'objet n'a pas été supprimé.
|
|
320
|
+
* @throws {ApiError} - Si l'objet a été supprimé.
|
|
321
|
+
*/
|
|
322
|
+
protected _checkNotDeleted(): void {
|
|
323
|
+
if (this._isDeleted) {
|
|
324
|
+
throw new ApiError(`Cet objet ${this.__entityTag} a été supprimé et ne peut plus être utilisé.`, 400);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Constructeur de l'entité.
|
|
330
|
+
*
|
|
331
|
+
* `parent` peut être :
|
|
332
|
+
* - une instance d'ApiClient
|
|
333
|
+
* - une instance d'entité (User, Organization, Project, Poi, Event, Badge, News)
|
|
334
|
+
*
|
|
335
|
+
* @param parent
|
|
336
|
+
* @param data - Données initiales.
|
|
337
|
+
* @param deps - Dépendances injectées (EndpointApi, autres entités).
|
|
338
|
+
* @param config - Configuration optionnelle (ex. `entityTag`).
|
|
339
|
+
* @throws {ApiError} Si `parent` est invalide ou si `deps.EndpointApi` n'est ni une instance ni un constructeur.
|
|
340
|
+
*/
|
|
341
|
+
constructor(parent: ApiClient | ParentLike, data: object = {}, deps: Partial<Deps> = {}, config: BaseEntityConfig = {}) {
|
|
342
|
+
this.__entityTag = config.entityTag || this.getEntityTag(this._getCtor().entityTag) || "BaseEntity";
|
|
343
|
+
this.deps = deps;
|
|
344
|
+
|
|
345
|
+
const isApiClientParent = this.getEntityTag(parent?.__entityTag) === "ApiClient";
|
|
346
|
+
|
|
347
|
+
if (isApiClientParent) {
|
|
348
|
+
this.apiClient = parent as ApiClient;
|
|
349
|
+
this.parent = null;
|
|
350
|
+
this.userContext = null;
|
|
351
|
+
} else if (parent && "apiClient" in parent) {
|
|
352
|
+
const p = parent as ParentLike;
|
|
353
|
+
this.apiClient = p.apiClient;
|
|
354
|
+
this.parent = p;
|
|
355
|
+
this.userContext = this.getEntityTag(p?.__entityTag) === "User" ? (p as User) : (p.userContext || null);
|
|
356
|
+
} else {
|
|
357
|
+
throw new ApiError("Parent invalide ou ApiClient manquant.", 400);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Gérer les deux cas : fonction constructeur ou instance
|
|
361
|
+
if (typeof deps.EndpointApi === "function") {
|
|
362
|
+
this.endpointApi = new deps.EndpointApi(this.apiClient);
|
|
363
|
+
} else if (typeof deps.EndpointApi === "object") {
|
|
364
|
+
this.endpointApi = deps.EndpointApi;
|
|
365
|
+
} else {
|
|
366
|
+
throw new ApiError("deps.EndpointApi doit être une classe ou une instance valide.", 500);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
this._serverData = reactive({});
|
|
370
|
+
|
|
371
|
+
const { draft, proxy, initial } = this._buildDraftAndProxy({
|
|
372
|
+
data: { ...data, ...this.defaultFields },
|
|
373
|
+
serverData: this._serverData,
|
|
374
|
+
constant: this._getCtor().SCHEMA_CONSTANTS,
|
|
375
|
+
apiClient: this.apiClient,
|
|
376
|
+
transforms: this.transforms,
|
|
377
|
+
removeFields: this.removeFields
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
this._initialDraftData = initial;
|
|
381
|
+
this._draftData = draft;
|
|
382
|
+
this.data = proxy;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Permet de récupérer le constructeur typé correctement.
|
|
387
|
+
*/
|
|
388
|
+
protected _getCtor(): BaseEntityCtor {
|
|
389
|
+
return this.constructor as BaseEntityCtor;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getEntityTag = (__entityTag: string | undefined) => __entityTag?.replace(/^_/, "");
|
|
393
|
+
|
|
394
|
+
/** @returns Identifiant de l'entité */
|
|
395
|
+
get id(): string | null {
|
|
396
|
+
return this._draftData.id || null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** @returns Slug de l'entité */
|
|
400
|
+
get slug(): string | null {
|
|
401
|
+
return this._draftData.slug || null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Définit un ID (utilisé en interne)
|
|
406
|
+
* @param newId - Nouvel ID à définir.
|
|
407
|
+
*/
|
|
408
|
+
_id(newId: string) {
|
|
409
|
+
this._draftData.id = newId;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** @returns Indique si l'utilisateur est connecté */
|
|
413
|
+
get isConnected(): boolean {
|
|
414
|
+
return this.apiClient.isConnected;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** @returns Identifiant utilisateur associé */
|
|
418
|
+
get userId(): string | null {
|
|
419
|
+
return this.apiClient.userId;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** @returns Données de brouillon courantes (réactif) */
|
|
423
|
+
get draftData(): ReactiveData {
|
|
424
|
+
return this._draftData;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** @returns Données de brouillon initiales (non-réactif) */
|
|
428
|
+
get initialDraftData(): Record<string, any> {
|
|
429
|
+
return this._initialDraftData;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** @returns Données brutes du serveur */
|
|
433
|
+
get serverData(): TServerData {
|
|
434
|
+
return this._serverData;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @returns Indique si cette entité représente l'utilisateur connecté */
|
|
438
|
+
get isMe(): boolean {
|
|
439
|
+
return !!(this.isConnected && this.userId && this.userContext?.id && typeof this.userId=== "string" && typeof this.userContext?.id === "string" && this.userId === this.userContext?.id);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** @returns Type de l'entité (ex: 'citoyens') */
|
|
443
|
+
getEntityType(): "citoyens" | "organizations" | "projects" | "events" | "poi" | "badges" | "news" | "comments" {
|
|
444
|
+
return this._getCtor().entityType;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Indique si le draft contient des modifications par rapport aux données initiales.
|
|
449
|
+
* @returns {boolean}
|
|
450
|
+
*/
|
|
451
|
+
hasChanges(): boolean {
|
|
452
|
+
const draftRaw = this._toRawDeep(this._draftData);
|
|
453
|
+
const draftSerialized = this._serialize(draftRaw);
|
|
454
|
+
const initialSerialized = this._serialize(this._initialDraftData);
|
|
455
|
+
|
|
456
|
+
// Comparer les chaînes JSON au lieu des références d'objets
|
|
457
|
+
return JSON.stringify(draftSerialized) !== JSON.stringify(initialSerialized);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Rafraîchit l'entité en rechargeant ses données depuis le serveur.
|
|
462
|
+
* @returns Données mises à jour
|
|
463
|
+
*/
|
|
464
|
+
async refresh(): Promise<Record<string, any>> {
|
|
465
|
+
if (!this.id) throw new ApiError("Impossible de rafraîchir sans ID.", 400);
|
|
466
|
+
return this.get();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Sauvegarde les modifications locales vers le serveur (add ou update).
|
|
471
|
+
* @returns Données serveur mises à jour (après éventuel `refresh()`)
|
|
472
|
+
*/
|
|
473
|
+
async save(): Promise<Record<string, any>> {
|
|
474
|
+
this._checkNotDeleted();
|
|
475
|
+
if (!this.isConnected) throw new ApiError("Non connecté.", 401);
|
|
476
|
+
this._calledFromSave = true;
|
|
477
|
+
try {
|
|
478
|
+
const payload = { ...this._draftData };
|
|
479
|
+
|
|
480
|
+
if (!this.id && typeof this._add === "function") {
|
|
481
|
+
await this._add(payload);
|
|
482
|
+
this._resetInitialDraftData();
|
|
483
|
+
// on refresh le contexte utilisateur si besoin
|
|
484
|
+
if(this.userContext) {
|
|
485
|
+
await this.userContext?.refresh();
|
|
486
|
+
}
|
|
487
|
+
return await this.refresh();
|
|
488
|
+
} else if (typeof this._update === "function") {
|
|
489
|
+
const hasChanged = await this._update(payload);
|
|
490
|
+
this._resetInitialDraftData();
|
|
491
|
+
if (hasChanged) return await this.refresh();
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return this._serverData;
|
|
495
|
+
} finally {
|
|
496
|
+
this._calledFromSave = false;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Crée une nouvelle instance d'entité à partir des données du serveur.
|
|
502
|
+
*
|
|
503
|
+
* @param data - Données du serveur.
|
|
504
|
+
* @param parent - Instance parente (ApiClient ou BaseEntity).
|
|
505
|
+
* @param deps - Dépendances injectées (classes d'entités disponibles).
|
|
506
|
+
* @returns Nouvelle instance d'entité.
|
|
507
|
+
*/
|
|
508
|
+
static fromServerData(
|
|
509
|
+
data: object,
|
|
510
|
+
parent: ApiClient | ParentLike,
|
|
511
|
+
deps: object
|
|
512
|
+
): BaseEntity<any> {
|
|
513
|
+
const instance = new this(parent, {}, deps);
|
|
514
|
+
instance._setData(data as any);
|
|
515
|
+
return instance;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Hook pour transformer les données du serveur avant qu'elles ne soient appliquées.
|
|
520
|
+
* Peut être overridé dans les classes filles pour transformer des champs imbriqués en instances d'entités.
|
|
521
|
+
*
|
|
522
|
+
* @param data - Les données brutes du serveur.
|
|
523
|
+
* @returns Les données transformées.
|
|
524
|
+
* @protected
|
|
525
|
+
*/
|
|
526
|
+
protected _transformServerData(data: TServerData): TServerData {
|
|
527
|
+
return data;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Met à jour les données de l'entité avec de nouvelles données.
|
|
532
|
+
*
|
|
533
|
+
* @param newData - Les nouvelles données à appliquer.
|
|
534
|
+
* @param {{ forceInitialDraftReset?: boolean }} [options]
|
|
535
|
+
* @protected
|
|
536
|
+
*/
|
|
537
|
+
protected _setData(newData: TServerData, { forceInitialDraftReset = false }: { forceInitialDraftReset?: boolean } = {}): void {
|
|
538
|
+
if (this.userContext && this.userContext !== (this as object)) {
|
|
539
|
+
this.apiClient._logger?.info?.(`[${this.__entityTag}] Mise à jour liée à userContext : ${this.userContext.id}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const transformed = this._transformServerData(newData);
|
|
543
|
+
const incoming = (transformed ?? {});
|
|
544
|
+
if (this._serverData && isReactive(this._serverData)) {
|
|
545
|
+
Object.assign(this._serverData, incoming);
|
|
546
|
+
} else {
|
|
547
|
+
this._serverData = reactive({ ...incoming }) as TServerData & ReactiveData;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const clientDraft = this._draftData ? this._toRawDeep(this._draftData) : {};
|
|
551
|
+
|
|
552
|
+
const mergedData = {
|
|
553
|
+
...(newData || {}),
|
|
554
|
+
...this.defaultFields,
|
|
555
|
+
...clientDraft
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const { draft, proxy, initial } = this._buildDraftAndProxy({
|
|
559
|
+
data: mergedData,
|
|
560
|
+
serverData: this._serverData,
|
|
561
|
+
previousDraft: this._draftData,
|
|
562
|
+
constant: this._getCtor().SCHEMA_CONSTANTS,
|
|
563
|
+
apiClient: this.apiClient,
|
|
564
|
+
transforms: this.transforms,
|
|
565
|
+
removeFields: this.removeFields
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
if (forceInitialDraftReset) {
|
|
569
|
+
this._initialDraftData = structuredClone(this._toRawDeep(draft));
|
|
570
|
+
} else if (!this._initialDraftData) {
|
|
571
|
+
this._initialDraftData = initial;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (isReactive(this._draftData)) {
|
|
575
|
+
this._updateDraftPreservingUserChanges(draft);
|
|
576
|
+
} else {
|
|
577
|
+
this._draftData = reactive(draft);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (!this.data) {
|
|
581
|
+
this.data = proxy;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_updateDraftPreservingUserChanges(draft: Record<string, any>): void {
|
|
587
|
+
for (const key of Object.keys(draft)) {
|
|
588
|
+
const current = this._draftData?.[key];
|
|
589
|
+
const initialValue = this._initialDraftData?.[key];
|
|
590
|
+
|
|
591
|
+
const isModified =
|
|
592
|
+
current !== undefined &&
|
|
593
|
+
initialValue !== undefined &&
|
|
594
|
+
this._serialize(this._toRawDeep(current)) !== this._serialize(initialValue);
|
|
595
|
+
|
|
596
|
+
if (!isModified) {
|
|
597
|
+
this._draftData[key] = draft[key];
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
_resetInitialDraftData(): void {
|
|
603
|
+
const raw = this._toRawDeep(this._draftData);
|
|
604
|
+
this._initialDraftData = structuredClone(raw);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
_toRawDeep(obj: any): any {
|
|
609
|
+
|
|
610
|
+
if (!this || typeof this._toRawDeep !== "function") {
|
|
611
|
+
throw new Error("`this._toRawDeep` is not bound correctly. Use a lambda to preserve context.");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (isSignal(obj)) {
|
|
615
|
+
return this._toRawDeep(obj.value);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Déproxifier les objets réactifs AVANT tout autre check
|
|
619
|
+
if (obj && typeof obj === "object" && obj.__isReactive && obj.__raw) {
|
|
620
|
+
return this._toRawDeep(obj.__raw);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Préserver les objets Date (avant le check générique "object")
|
|
624
|
+
if (obj instanceof Date) {
|
|
625
|
+
return obj;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Préserver les types d'upload pour éviter leur sérialisation incorrecte
|
|
629
|
+
// Buffer (Node.js)
|
|
630
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(obj)) {
|
|
631
|
+
return obj;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// File (Browser)
|
|
635
|
+
if (typeof File !== "undefined" && obj instanceof File) {
|
|
636
|
+
return obj;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Blob (Browser)
|
|
640
|
+
if (typeof Blob !== "undefined" && obj instanceof Blob) {
|
|
641
|
+
return obj;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// ReadableStream (Node.js) - détecté via les méthodes pipe et on
|
|
645
|
+
if (obj && typeof obj.pipe === "function" && typeof obj.on === "function") {
|
|
646
|
+
return obj;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (Array.isArray(obj)) {
|
|
650
|
+
return obj.map((item) => this._toRawDeep(item));
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (typeof obj === "object" && obj !== null) {
|
|
654
|
+
const result: any = {};
|
|
655
|
+
for (const key of Object.keys(obj)) {
|
|
656
|
+
result[key] = this._toRawDeep(obj[key]);
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return obj; // valeur primitive
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Champs à ajouter automatiquement à chaque draft (ex: `typeElement`).
|
|
666
|
+
* Souvent utilisés dans les conditions `if/then` du JSON Schema.
|
|
667
|
+
*/
|
|
668
|
+
defaultFields: { [key: string]: any } = {};
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Champs à exclure explicitement du draft et des payloads.
|
|
672
|
+
*/
|
|
673
|
+
removeFields: string[] = [];
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Transformations à appliquer à certains champs lors de la lecture depuis le draft.
|
|
677
|
+
* Clé = champ, valeur = fonction (val, full) => valeur transformée.
|
|
678
|
+
*/
|
|
679
|
+
transforms: TransformsMap = {};
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* ───────────────────────────────
|
|
683
|
+
* JSON
|
|
684
|
+
* ───────────────────────────────
|
|
685
|
+
*/
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Convertit l'instance en JSON pour l'envoi au serveur.
|
|
689
|
+
*
|
|
690
|
+
* @returns Représentation JSON de l'instance.
|
|
691
|
+
*/
|
|
692
|
+
toJSON(): { __entityTag: string; __isSerializedEntity: boolean; serverData: any; parent: { id?: string; type?: string; __entityTag?: string; slug?: string } | null } {
|
|
693
|
+
const parentMeta: { id?: string; type?: string; __entityTag?: string; slug?: string } = {};
|
|
694
|
+
if (this.parent?.id) parentMeta.id = this.parent.id;
|
|
695
|
+
if (typeof this.parent?.getEntityType === "function") parentMeta.type = this.parent.getEntityType();
|
|
696
|
+
if (this.parent?.__entityTag) parentMeta.__entityTag = this.parent.__entityTag;
|
|
697
|
+
if (this.parent?.slug) parentMeta.slug = this.parent.slug;
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
__entityTag: this.__entityTag,
|
|
701
|
+
__isSerializedEntity: true,
|
|
702
|
+
serverData: this._serialize(this._serverData),
|
|
703
|
+
parent: Object.keys(parentMeta).length > 0 ? parentMeta : null
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
_serialize(obj: any): any {
|
|
709
|
+
try {
|
|
710
|
+
return JSON.parse(EJSON.stringify(this._removeUnserializables(obj)));
|
|
711
|
+
} catch (e) {
|
|
712
|
+
const error = e as Error;
|
|
713
|
+
this.apiClient?._logger?.error?.("Erreur de sérialisation EJSON:", {
|
|
714
|
+
message: error?.message,
|
|
715
|
+
stack: error?.stack,
|
|
716
|
+
error: e
|
|
717
|
+
});
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Supprime les propriétés non sérialisables d'un objet.
|
|
724
|
+
*
|
|
725
|
+
* @param obj - L'objet à nettoyer.
|
|
726
|
+
* @param seen - Ensemble pour éviter les références circulaires.
|
|
727
|
+
* @returns L'objet nettoyé.
|
|
728
|
+
* @private
|
|
729
|
+
*/
|
|
730
|
+
private _removeUnserializables(obj: any, seen = new WeakSet<object>()): any {
|
|
731
|
+
if (obj === null || typeof obj !== "object") return obj;
|
|
732
|
+
|
|
733
|
+
if (seen.has(obj)) return null;
|
|
734
|
+
seen.add(obj);
|
|
735
|
+
|
|
736
|
+
// Ignore les proxys réactifs
|
|
737
|
+
if (obj.__isReactive && typeof obj.__raw === "object") {
|
|
738
|
+
return this._removeUnserializables(obj.__raw, seen);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (Array.isArray(obj)) {
|
|
742
|
+
return obj.map((el) => this._removeUnserializables(el, seen));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const clean: Record<string, any> = {};
|
|
746
|
+
for (const key of Object.keys(obj)) {
|
|
747
|
+
const val = obj[key];
|
|
748
|
+
|
|
749
|
+
if (
|
|
750
|
+
typeof val === "function" ||
|
|
751
|
+
typeof val === "symbol" ||
|
|
752
|
+
typeof val === "undefined"
|
|
753
|
+
) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
try {
|
|
758
|
+
clean[key] = this._removeUnserializables(val, seen);
|
|
759
|
+
|
|
760
|
+
} catch {
|
|
761
|
+
clean[key] = null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return clean;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Restaure les données sérialisées en un objet d'origine.
|
|
770
|
+
*
|
|
771
|
+
* @param obj - L'objet à restaurer.
|
|
772
|
+
* @returns L'objet restauré.
|
|
773
|
+
*/
|
|
774
|
+
static _revive(obj: object): any {
|
|
775
|
+
return EJSON.fromJSONValue(obj);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Crée une instance d'entité à partir de données JSON.
|
|
780
|
+
*
|
|
781
|
+
* @param json - Données JSON à utiliser.
|
|
782
|
+
* @param parent - Instance parente.
|
|
783
|
+
* @param deps - Dépendances injectées.
|
|
784
|
+
* @returns Nouvelle instance d'entité.
|
|
785
|
+
*/
|
|
786
|
+
static fromJSON(
|
|
787
|
+
json: unknown,
|
|
788
|
+
parent?: ApiClient | BaseEntity<any> | null,
|
|
789
|
+
deps?: object
|
|
790
|
+
): BaseEntity<any> {
|
|
791
|
+
if (!json || typeof json !== "object" || !("serverData" in json)) {
|
|
792
|
+
throw new Error("Invalid JSON format: missing serverData");
|
|
793
|
+
}
|
|
794
|
+
const { serverData } = json as { serverData: object };
|
|
795
|
+
|
|
796
|
+
const instance = this.fromServerData(this._revive(serverData), parent as ApiClient | ParentLike, deps || {});
|
|
797
|
+
|
|
798
|
+
return instance;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* ───────────────────────────────
|
|
803
|
+
* UtilMixin
|
|
804
|
+
* ───────────────────────────────
|
|
805
|
+
*/
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Appelle une méthode de l'API.
|
|
809
|
+
*
|
|
810
|
+
* @param constant - Le nom de la méthode à appeler.
|
|
811
|
+
* @param data - Les données à passer à la méthode.
|
|
812
|
+
* @returns - `response.data` transformé ; peut être `null` si la requête est **mise en file** (offline/breaker).
|
|
813
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
814
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
815
|
+
* @throws {ApiClientError} - Si l'utilisateur n'est pas authentifié.
|
|
816
|
+
*/
|
|
817
|
+
async call(constant: string, data: object = {}): Promise<unknown> {
|
|
818
|
+
return this.apiClient.safeCall(async () => {
|
|
819
|
+
const response = await this.apiClient.callEndpoint(constant, data);
|
|
820
|
+
this.apiClient.checkAndThrowApiResponseError(response);
|
|
821
|
+
return response.data;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Appelle une méthode de l'API si l'utilisateur n'est pas connecté.
|
|
827
|
+
*
|
|
828
|
+
* @param constant - Le nom de la méthode à appeler.
|
|
829
|
+
* @param data - Les données à passer à la méthode.
|
|
830
|
+
* @returns - `response.data` ou `null` si enqueue offline/breaker.
|
|
831
|
+
* @throws {ApiAuthenticationError} - Si l'utilisateur est connecté.
|
|
832
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
833
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
834
|
+
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
835
|
+
*/
|
|
836
|
+
async callNoConnected(constant: string, data: object = {}): Promise<unknown> {
|
|
837
|
+
if(this.isConnected) {
|
|
838
|
+
throw new ApiAuthenticationError("Vous devez ne devez pas être connecté pour faire cette action.", 403);
|
|
839
|
+
}
|
|
840
|
+
return this.call(constant, data);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Appelle une méthode de l'API si l'utilisateur est connecté.
|
|
845
|
+
*
|
|
846
|
+
* @param param - Le nom de la méthode à appeler ou une fonction de rappel.
|
|
847
|
+
* @param data - Les données à passer à la méthode.
|
|
848
|
+
* @returns - `response.data` ou `null` si enqueue offline/breaker.
|
|
849
|
+
* @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas connecté.
|
|
850
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
851
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
852
|
+
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
853
|
+
*/
|
|
854
|
+
async callIsConnected(param: string | (() => Promise<any>), data: object = {}): Promise<unknown> {
|
|
855
|
+
if(!this.isConnected) {
|
|
856
|
+
throw new ApiAuthenticationError("Vous devez être connecté pour faire cette action.", 401);
|
|
857
|
+
}
|
|
858
|
+
// Si le premier paramètre est une fonction, on l'exécute en tant que callback
|
|
859
|
+
if (typeof param === "function") {
|
|
860
|
+
return await param();
|
|
861
|
+
}
|
|
862
|
+
return this.call(param, data);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Appelle une méthode de l'API si l'utilisateur est lui-même.
|
|
867
|
+
*
|
|
868
|
+
* @param param - Le nom de la méthode à appeler ou une fonction de rappel.
|
|
869
|
+
* @param data - Les données à passer à la méthode.
|
|
870
|
+
* @returns - `response.data` ou `null` si enqueue offline/breaker.
|
|
871
|
+
* @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas lui-même.
|
|
872
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
873
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
874
|
+
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
875
|
+
*/
|
|
876
|
+
async callIsMe(param: string | (() => Promise<any>), data: object = {}): Promise<unknown> {
|
|
877
|
+
if (!this.isMe) {
|
|
878
|
+
throw new ApiAuthenticationError("Vous devez être vous-même pour faire cette action.", 403);
|
|
879
|
+
}
|
|
880
|
+
// Si le premier paramètre est une fonction, on l'exécute en tant que callback
|
|
881
|
+
if (typeof param === "function") {
|
|
882
|
+
return await param();
|
|
883
|
+
}
|
|
884
|
+
// Sinon, on considère qu'il s'agit d'un constant et on appelle la méthode par défaut
|
|
885
|
+
return await this.callIsConnected(param, data);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Valide une image d'entrée.
|
|
890
|
+
*
|
|
891
|
+
* @param imageInput - L'image à valider.
|
|
892
|
+
* @returns - L'image validée.
|
|
893
|
+
* @private
|
|
894
|
+
*/
|
|
895
|
+
protected async _validateImage(imageInput: UploadInput): Promise<ValidatedUpload> {
|
|
896
|
+
const image = await this._validateUploadInput(imageInput, {
|
|
897
|
+
allowedMimeTypes: ["image/jpeg", "image/png", "image/jpg"],
|
|
898
|
+
expectedType: "image"
|
|
899
|
+
});
|
|
900
|
+
return image;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Valide un fichier d'entrée.
|
|
905
|
+
*
|
|
906
|
+
* @param fileInput - Le fichier à valider.
|
|
907
|
+
* @returns - Le fichier validé.
|
|
908
|
+
* @private
|
|
909
|
+
*/
|
|
910
|
+
protected async _validateFile(fileInput: UploadInput): Promise<ValidatedUpload> {
|
|
911
|
+
const file = await this._validateUploadInput(fileInput, {
|
|
912
|
+
allowedMimeTypes: ["application/pdf", "text/plain", "text/csv"],
|
|
913
|
+
expectedType: "file"
|
|
914
|
+
});
|
|
915
|
+
return file;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Valide les entrées d'upload de fichiers.
|
|
920
|
+
*
|
|
921
|
+
* @param input - Le fichier à valider.
|
|
922
|
+
* @param {{ allowedMimeTypes?: string[], expectedType?: "image"|"file"|"any" }} options - Options de validation.
|
|
923
|
+
* @returns - Le fichier validé.
|
|
924
|
+
* @throws {ApiValidationError} - Si le type de fichier est invalide.
|
|
925
|
+
* @throws {Error} - Si le type de fichier est inconnu.
|
|
926
|
+
* @private
|
|
927
|
+
*/
|
|
928
|
+
private async _validateUploadInput(input: UploadInput, { allowedMimeTypes = [], expectedType = "any" }: { allowedMimeTypes?: string[]; expectedType?: string }): Promise<ValidatedUpload> {
|
|
929
|
+
if (!input) {
|
|
930
|
+
throw new ApiValidationError("Le fichier est requis.", 400, ["Le fichier est requis"]);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const isNode = typeof window === "undefined" && typeof process !== "undefined";
|
|
934
|
+
|
|
935
|
+
let output: ValidatedUpload | null = null;
|
|
936
|
+
let mimeType = "";
|
|
937
|
+
|
|
938
|
+
// Navigateur : File
|
|
939
|
+
if (typeof File !== "undefined" && input instanceof File) {
|
|
940
|
+
mimeType = input.type;
|
|
941
|
+
if (!allowedMimeTypes.includes(mimeType)) {
|
|
942
|
+
throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Navigateur : Blob
|
|
947
|
+
else if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
948
|
+
mimeType = input.type;
|
|
949
|
+
if (!allowedMimeTypes.includes(mimeType)) {
|
|
950
|
+
throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
const ext = mimeType.split("/")[1] || "bin";
|
|
954
|
+
const fileName = `${Date.now()}.${ext}`;
|
|
955
|
+
output = new File([input], fileName, { type: mimeType });
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// Node.js : Buffer
|
|
959
|
+
else if (isNode && Buffer.isBuffer(input)) {
|
|
960
|
+
const fileTypeResult = await fromBuffer(input);
|
|
961
|
+
|
|
962
|
+
if (!fileTypeResult) {
|
|
963
|
+
throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
mimeType = fileTypeResult.mime;
|
|
967
|
+
|
|
968
|
+
if (!allowedMimeTypes.includes(mimeType)) {
|
|
969
|
+
throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Pour un fichier image, on transforme en stream
|
|
973
|
+
if (expectedType === "image") {
|
|
974
|
+
const ext = fileTypeResult.ext;
|
|
975
|
+
const filename = `${Date.now()}.${ext}`;
|
|
976
|
+
output = await this._createReadStreamFromBuffer(input, filename, mimeType);
|
|
977
|
+
} else {
|
|
978
|
+
output = input;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Node.js : ReadableStream
|
|
983
|
+
else if (isNode && isNodeReadable(input)) {
|
|
984
|
+
const readableIn = input as import("stream").Readable;
|
|
985
|
+
|
|
986
|
+
const previewChunks: Buffer[] = [];
|
|
987
|
+
const tee = await this._passThrough();
|
|
988
|
+
const resultStream = await this._passThrough(); // PassThrough est aussi un Readable
|
|
989
|
+
|
|
990
|
+
const MAX_BYTES = 4100;
|
|
991
|
+
let bytesRead = 0;
|
|
992
|
+
|
|
993
|
+
readableIn.on("data", (chunk) => {
|
|
994
|
+
if (bytesRead < MAX_BYTES) {
|
|
995
|
+
previewChunks.push(chunk);
|
|
996
|
+
bytesRead += chunk.length;
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
readableIn.pipe(tee).pipe(resultStream);
|
|
1001
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1002
|
+
|
|
1003
|
+
const previewBuffer = Buffer.concat(previewChunks);
|
|
1004
|
+
const fileTypeResult = await fromBuffer(previewBuffer);
|
|
1005
|
+
if (!fileTypeResult) {
|
|
1006
|
+
throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
|
|
1007
|
+
}
|
|
1008
|
+
mimeType = fileTypeResult.mime;
|
|
1009
|
+
|
|
1010
|
+
if (!allowedMimeTypes.includes(mimeType)) {
|
|
1011
|
+
throw new ApiValidationError("Le type du fichier est invalide.", 400, ["Le type du fichier est invalide"]);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// on annote explicitement pour TS
|
|
1015
|
+
(resultStream as ReadableWithMeta).path = `${Date.now()}.${fileTypeResult.ext}`;
|
|
1016
|
+
(resultStream as ReadableWithMeta).mimeType = mimeType;
|
|
1017
|
+
|
|
1018
|
+
output = resultStream as ReadableWithMeta;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
else {
|
|
1022
|
+
throw new ApiValidationError("Type de fichier non reconnu.", 400, ["Type de fichier non reconnu."]);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
return output!;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Transforme un Buffer en ReadableStream équivalent à fs.createReadStream
|
|
1030
|
+
* @param buffer - Le buffer contenant les données binaires
|
|
1031
|
+
* @param [filename="file.bin"] - Nom de fichier (utilisé dans FormData)
|
|
1032
|
+
* @param [mimeType="application/octet-stream"] - Type MIME (utilisé dans FormData)
|
|
1033
|
+
* @returns - Readable doté de `path` et `mimeType`
|
|
1034
|
+
* @private
|
|
1035
|
+
*/
|
|
1036
|
+
private async _createReadStreamFromBuffer(buffer: Buffer, filename = "file.bin", mimeType = "application/octet-stream"): Promise<ReadableWithMeta> {
|
|
1037
|
+
const stream = await this._bufferToReadable(buffer);
|
|
1038
|
+
(stream as ReadableWithMeta).path = filename;
|
|
1039
|
+
(stream as ReadableWithMeta).mimeType = mimeType;
|
|
1040
|
+
return stream as ReadableWithMeta;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Transforme un Buffer en ReadableStream.
|
|
1045
|
+
*
|
|
1046
|
+
* @param buffer - Le buffer à transformer.
|
|
1047
|
+
* @returns - Un ReadableStream.
|
|
1048
|
+
* @throws {Error} - Si appelé dans le navigateur.
|
|
1049
|
+
* @private
|
|
1050
|
+
*/
|
|
1051
|
+
private async _bufferToReadable(buffer: Buffer): Promise<import("stream").Readable> {
|
|
1052
|
+
if (typeof window === "undefined") {
|
|
1053
|
+
const { bufferToReadable } = await import("../utils/stream-utils.node.js");
|
|
1054
|
+
return bufferToReadable(buffer);
|
|
1055
|
+
} else {
|
|
1056
|
+
throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Crée un PassThrough stream pour le traitement des fichiers.
|
|
1062
|
+
*
|
|
1063
|
+
* @returns - Un PassThrough stream.
|
|
1064
|
+
* @throws {Error} - Si appelé dans le navigateur.
|
|
1065
|
+
* @private
|
|
1066
|
+
*/
|
|
1067
|
+
private async _passThrough(): Promise<import("stream").PassThrough> {
|
|
1068
|
+
if (typeof window === "undefined") {
|
|
1069
|
+
const { createPassThrough } = await import("../utils/stream-utils.node.js");
|
|
1070
|
+
return createPassThrough();
|
|
1071
|
+
} else {
|
|
1072
|
+
throw new Error("passThrough ne doit pas être appelé dans le navigateur");
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// async _bufferToReadable(buffer) {
|
|
1077
|
+
// if (typeof window === "undefined") {
|
|
1078
|
+
// const { Readable } = await import("stream");
|
|
1079
|
+
// return Readable.from(buffer);
|
|
1080
|
+
// } else {
|
|
1081
|
+
// throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
|
|
1082
|
+
// }
|
|
1083
|
+
// },
|
|
1084
|
+
|
|
1085
|
+
// async _passThrough() {
|
|
1086
|
+
// if (typeof window === "undefined") {
|
|
1087
|
+
// const { PassThrough } = await import("stream");
|
|
1088
|
+
// return new PassThrough();
|
|
1089
|
+
// } else {
|
|
1090
|
+
// throw new Error("passThrough ne doit pas être appelé dans le navigateur");
|
|
1091
|
+
// }
|
|
1092
|
+
// },
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Supprime les propriétés d'un objet en fonction d'une liste de clés.
|
|
1096
|
+
*
|
|
1097
|
+
* @param obj - L'objet source.
|
|
1098
|
+
* @param propsToRemove - Liste des clés à supprimer
|
|
1099
|
+
* @returns - Un nouvel objet sans les propriétés supprimées.
|
|
1100
|
+
* @private
|
|
1101
|
+
*/
|
|
1102
|
+
private _omitProps(obj: object, propsToRemove: string[]): Record<string, any> {
|
|
1103
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1104
|
+
|
|
1105
|
+
const result: Record<string, any> = { ...obj };
|
|
1106
|
+
for (const prop of propsToRemove) {
|
|
1107
|
+
delete result[prop];
|
|
1108
|
+
}
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Extrait les propriétés d'un objet en fonction d'une liste de clés.
|
|
1114
|
+
*
|
|
1115
|
+
* @param obj - L'objet source.
|
|
1116
|
+
* @param keys - Liste des clés à extraire.
|
|
1117
|
+
* @param [transforms={}] - Transformations à appliquer aux valeurs.
|
|
1118
|
+
* @returns - Un nouvel objet contenant les propriétés extraites.
|
|
1119
|
+
* @private
|
|
1120
|
+
*/
|
|
1121
|
+
private _pickProps(obj: any, keys: string[], transforms: TransformsMap = {}): Record<string, any> {
|
|
1122
|
+
if (!obj || typeof obj !== "object") return {};
|
|
1123
|
+
|
|
1124
|
+
const result: Record<string, any> = {};
|
|
1125
|
+
|
|
1126
|
+
for (const key of keys) {
|
|
1127
|
+
if (key in obj) {
|
|
1128
|
+
const value = obj[key];
|
|
1129
|
+
if (typeof transforms[key] === "function") {
|
|
1130
|
+
result[key] = transforms[key](value, obj); // (valeur, objet source)
|
|
1131
|
+
} else {
|
|
1132
|
+
result[key] = value;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
return result;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Vérifie si au moins une clé est présente dans l'objet et n'est pas nulle.
|
|
1142
|
+
*
|
|
1143
|
+
* @param obj - L'objet à vérifier.
|
|
1144
|
+
* @param keys - Liste des clés à vérifier.
|
|
1145
|
+
* @returns - true si au moins une clé est présente et non nulle, sinon false.
|
|
1146
|
+
* @private
|
|
1147
|
+
*/
|
|
1148
|
+
private _hasAtLeastOne(obj: any, keys: string[] = []): boolean {
|
|
1149
|
+
return keys.some((key) => key in obj && obj[key] != null);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Crée un proxy filtré pour une liste d'entités.
|
|
1154
|
+
* @private
|
|
1155
|
+
*/
|
|
1156
|
+
_createFilteredProxy<T extends Deletable>(list: T[]): T[] {
|
|
1157
|
+
return new Proxy(list, {
|
|
1158
|
+
get(target, prop, receiver) {
|
|
1159
|
+
const active = target.filter(n => !n._isDeleted);
|
|
1160
|
+
|
|
1161
|
+
if (prop === "length") return active.length;
|
|
1162
|
+
|
|
1163
|
+
if (typeof prop === "string" && /^\d+$/.test(prop)) {
|
|
1164
|
+
return active[Number(prop)];
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// Récupère la propriété depuis la vue filtrée
|
|
1168
|
+
const value = Reflect.get(active, prop, receiver);
|
|
1169
|
+
|
|
1170
|
+
// Si c’est une méthode, lier à 'active' (une seule évaluation du filter)
|
|
1171
|
+
return typeof value === "function" ? value.bind(active) : value;
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Génère un nouvel identifiant unique.
|
|
1178
|
+
* @returns Un identifiant unique.
|
|
1179
|
+
* @protected
|
|
1180
|
+
*/
|
|
1181
|
+
protected _newId(): string {
|
|
1182
|
+
return createObjectId().toHexString();
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* ───────────────────────────────
|
|
1187
|
+
* DraftStateMixin
|
|
1188
|
+
* ───────────────────────────────
|
|
1189
|
+
*/
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* Recalcule les champs autorisés en fonction de l'état actuel du draft.
|
|
1193
|
+
* Utilisé pour validation dynamique dans le proxy this.data.
|
|
1194
|
+
* @returns Liste des champs autorisés
|
|
1195
|
+
* @private
|
|
1196
|
+
*/
|
|
1197
|
+
private _getAllowedFieldsForCurrentState(): string[] {
|
|
1198
|
+
if (!this._allowedFieldsMetadata) {
|
|
1199
|
+
return this._allowedFieldsCache || [];
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const { combinedSchema, removeFields } = this._allowedFieldsMetadata;
|
|
1203
|
+
|
|
1204
|
+
// Combiner defaultFields + draft pour évaluer les conditions if/then
|
|
1205
|
+
// typeElement vient de defaultFields et ne change jamais
|
|
1206
|
+
const rawDraft = this._toRawDeep(this._draftData);
|
|
1207
|
+
const currentData = { ...this.defaultFields, ...rawDraft };
|
|
1208
|
+
|
|
1209
|
+
let allowed = this._extractWritableFields(combinedSchema, currentData);
|
|
1210
|
+
|
|
1211
|
+
if (currentData.id && !allowed.includes("id")) {
|
|
1212
|
+
allowed.push("id");
|
|
1213
|
+
}
|
|
1214
|
+
if (currentData.slug && !allowed.includes("slug")) {
|
|
1215
|
+
allowed.push("slug");
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
allowed = allowed.filter(k => !removeFields.includes(k));
|
|
1219
|
+
|
|
1220
|
+
return allowed;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* Crée un proxy combinant draft + serveur, avec transformations facultatives.
|
|
1225
|
+
* @param apiClient
|
|
1226
|
+
* @param [server={}]
|
|
1227
|
+
* @param [draft={}]
|
|
1228
|
+
* @param _allowedFields - champs autorisés dans le draft (fallback si pas de metadata)
|
|
1229
|
+
* @param [transforms={}] - transformateurs de lecture
|
|
1230
|
+
* @param {{ throwOnError?: boolean }} [options={}] - options
|
|
1231
|
+
* @param [entity=this] - référence à l'entité pour recalcul dynamique
|
|
1232
|
+
* @returns {object}
|
|
1233
|
+
* @private
|
|
1234
|
+
*/
|
|
1235
|
+
private _createDraftProxy(apiClient: ApiClient, server: Record<string, unknown> = {}, draft: Record<string, unknown> = {}, _allowedFields: string[] = [], transforms: TransformsMap = {}, options: { throwOnError?: boolean } = {}, entity: BaseEntity<any> = this): Record<string, unknown> {
|
|
1236
|
+
return new Proxy({}, {
|
|
1237
|
+
get: (_, prop) => {
|
|
1238
|
+
if (typeof prop !== "string" && typeof prop !== "symbol") return undefined;
|
|
1239
|
+
|
|
1240
|
+
// 1) Draft prioritaire
|
|
1241
|
+
if (typeof prop === "string" && prop in draft) {
|
|
1242
|
+
const value = draft[prop];
|
|
1243
|
+
const transformer = transforms[prop];
|
|
1244
|
+
return typeof transformer === "function" ? transformer(value, draft) : value;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// 2) Fallback serveur
|
|
1248
|
+
if (server && typeof server === "object" && typeof prop === "string" && prop in server) {
|
|
1249
|
+
const value = server[prop];
|
|
1250
|
+
const transformer = transforms[prop];
|
|
1251
|
+
return typeof transformer === "function" ? transformer(value, server) : value;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
return undefined;
|
|
1255
|
+
},
|
|
1256
|
+
|
|
1257
|
+
set: (_, prop, value) => {
|
|
1258
|
+
if (typeof prop !== "string") {
|
|
1259
|
+
// On refuse tout ce qui n'est pas un champ "string"
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Recalcul dynamique des champs autorisés basé sur l'état actuel
|
|
1264
|
+
const currentAllowed = entity._getAllowedFieldsForCurrentState();
|
|
1265
|
+
|
|
1266
|
+
if (!currentAllowed.includes(prop)) {
|
|
1267
|
+
const message = `[DraftProxy] Le champ "${prop}" n'est pas autorisé.`;
|
|
1268
|
+
if (options.throwOnError) {
|
|
1269
|
+
throw new ApiValidationError(message, 400, [message], { field: prop, value, allowedFields: currentAllowed });
|
|
1270
|
+
}
|
|
1271
|
+
apiClient._logger?.warn?.(message);
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const current = draft[prop];
|
|
1276
|
+
if (current && typeof current === "object" && (current as any).__isSignal === true) {
|
|
1277
|
+
(current as any).value = value; // met à jour le signal
|
|
1278
|
+
} else {
|
|
1279
|
+
draft[prop] = value; // fallback classique
|
|
1280
|
+
}
|
|
1281
|
+
return true;
|
|
1282
|
+
},
|
|
1283
|
+
|
|
1284
|
+
deleteProperty: (_, prop) => {
|
|
1285
|
+
if (typeof prop !== "string") return false;
|
|
1286
|
+
const currentAllowed = entity._getAllowedFieldsForCurrentState();
|
|
1287
|
+
if (!currentAllowed.includes(prop)) return false;
|
|
1288
|
+
delete draft[prop];
|
|
1289
|
+
return true;
|
|
1290
|
+
},
|
|
1291
|
+
|
|
1292
|
+
has: (_, prop) => (typeof prop === "string" ? (prop in draft || (server && typeof server === "object" && prop in server)) : false),
|
|
1293
|
+
|
|
1294
|
+
ownKeys: () => {
|
|
1295
|
+
const keys = new Set([
|
|
1296
|
+
...Object.keys(server || {}),
|
|
1297
|
+
...Object.keys(draft || {}),
|
|
1298
|
+
]);
|
|
1299
|
+
return Array.from(keys);
|
|
1300
|
+
},
|
|
1301
|
+
|
|
1302
|
+
getOwnPropertyDescriptor: (_, prop) => {
|
|
1303
|
+
if (typeof prop !== "string") return undefined;
|
|
1304
|
+
if (prop in draft || (server && typeof server === "object" && prop in server)) {
|
|
1305
|
+
return { enumerable: true, configurable: true };
|
|
1306
|
+
}
|
|
1307
|
+
return undefined;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Extrait les champs modifiables d'un schéma JSON.
|
|
1314
|
+
*
|
|
1315
|
+
* @param schema - Le schéma JSON à analyser.
|
|
1316
|
+
* @param data - Les données à comparer.
|
|
1317
|
+
* @param ctx - Contexte d'extraction (pour la récursion).
|
|
1318
|
+
* @param ctx.defs - Définitions de schéma.
|
|
1319
|
+
* @param ctx.visited - Ensemble des schémas déjà visités.
|
|
1320
|
+
* @returns Liste des champs modifiables (propriétés `writeable` implicites).
|
|
1321
|
+
* @private
|
|
1322
|
+
*/
|
|
1323
|
+
private _extractWritableFields(schema: JsonSchema = {}, data: Record<string, unknown> = {}, ctx: { defs: Record<string, JsonSchema>; visited: Set<string> } = { defs: {}, visited: new Set() }): string[] {
|
|
1324
|
+
if (!schema || typeof schema !== "object") return [];
|
|
1325
|
+
if (schema.$id && ctx.visited.has(schema.$id)) return [];
|
|
1326
|
+
if (schema.$id) ctx.visited.add(schema.$id);
|
|
1327
|
+
ctx.defs = schema.$defs || schema.definitions || ctx.defs;
|
|
1328
|
+
|
|
1329
|
+
const fields: string[] = [];
|
|
1330
|
+
|
|
1331
|
+
if (schema.$ref) {
|
|
1332
|
+
const refKey = schema.$ref.replace(/^#\/?(\$defs|definitions)\//, "");
|
|
1333
|
+
const resolved = ctx.defs[refKey];
|
|
1334
|
+
if (resolved) fields.push(...this._extractWritableFields(resolved, data, ctx));
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
if (schema.allOf) {
|
|
1338
|
+
schema.allOf.forEach((s) => fields.push(...this._extractWritableFields(s, data, ctx)));
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (schema.if && schema.then) {
|
|
1342
|
+
const condition = schema.if.properties;
|
|
1343
|
+
let matches = true;
|
|
1344
|
+
if (condition) {
|
|
1345
|
+
for (const key in condition) {
|
|
1346
|
+
const expected = condition[key]?.const;
|
|
1347
|
+
if (data[key] !== expected) {
|
|
1348
|
+
matches = false;
|
|
1349
|
+
break;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (matches && schema.then) {
|
|
1355
|
+
fields.push(...this._extractWritableFields(schema.then, data, ctx));
|
|
1356
|
+
} else if (!matches && schema.else) {
|
|
1357
|
+
fields.push(...this._extractWritableFields(schema.else, data, ctx));
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (schema.properties) {
|
|
1362
|
+
fields.push(...Object.entries(schema.properties)
|
|
1363
|
+
|
|
1364
|
+
.filter(([_, def]) => def.readOnly !== true && def.const === undefined)
|
|
1365
|
+
.map(([key]) => key));
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return Array.from(new Set(fields));
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Construit un brouillon et un proxy à partir des données et du schéma.
|
|
1373
|
+
*
|
|
1374
|
+
* @param options - Options de construction.
|
|
1375
|
+
* @param options.data - Données à utiliser pour le brouillon.
|
|
1376
|
+
* @param options.serverData - Données du serveur.
|
|
1377
|
+
* @param options.previousDraft - Brouillon précédent pour la réactivité.
|
|
1378
|
+
* @param options.constant - Nom de la constante ou tableau de constantes.
|
|
1379
|
+
* @param options.apiClient - Instance de l'API.
|
|
1380
|
+
* @param options.transforms - Transformations à appliquer.
|
|
1381
|
+
* @param options.throwOnError - Si vrai, lève une erreur en cas de problème.
|
|
1382
|
+
* @param options.removeFields - Liste des champs à ignorer.
|
|
1383
|
+
* @returns Draft réactif, proxy combiné, snapshot initial.
|
|
1384
|
+
* @private
|
|
1385
|
+
*/
|
|
1386
|
+
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> } {
|
|
1387
|
+
const constants = Array.isArray(constant) ? constant : [constant];
|
|
1388
|
+
const combinedSchema: {allOf: JsonSchema[], $defs: Record<string, JsonSchema>} = {
|
|
1389
|
+
allOf: [],
|
|
1390
|
+
$defs: {}
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
for (const key of constants) {
|
|
1394
|
+
const sch = apiClient.getRequestSchema(key);
|
|
1395
|
+
if (!sch) throw new ApiError(`Unable to find schema for ${key}.`, 404);
|
|
1396
|
+
|
|
1397
|
+
// Extraire et fusionner les $defs
|
|
1398
|
+
if (sch.$defs) {
|
|
1399
|
+
for (const [defKey, defVal] of Object.entries(sch.$defs)) {
|
|
1400
|
+
if (combinedSchema.$defs[defKey]) {
|
|
1401
|
+
apiClient._logger.warn(`Duplicate $defs key '${defKey}' from schema '${key}'`);
|
|
1402
|
+
} else {
|
|
1403
|
+
combinedSchema.$defs[defKey] = defVal as JsonSchema;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
combinedSchema.allOf.push(sch as JsonSchema);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
let allowed = this._extractWritableFields(combinedSchema, data);
|
|
1412
|
+
|
|
1413
|
+
if (data.id && allowed.indexOf("id") === -1) {
|
|
1414
|
+
allowed.push("id");
|
|
1415
|
+
}
|
|
1416
|
+
if (data.slug && allowed.indexOf("slug") === -1) {
|
|
1417
|
+
allowed.push("slug");
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
allowed = allowed.filter(k => !removeFields.includes(k));
|
|
1421
|
+
|
|
1422
|
+
// Stocker metadata pour recalcul dynamique des allowedFields dans le proxy
|
|
1423
|
+
this._allowedFieldsCache = allowed;
|
|
1424
|
+
this._allowedFieldsMetadata = {
|
|
1425
|
+
combinedSchema,
|
|
1426
|
+
transforms,
|
|
1427
|
+
removeFields
|
|
1428
|
+
};
|
|
1429
|
+
|
|
1430
|
+
// Transformation des champs autorisés
|
|
1431
|
+
const rawDraft = Object.fromEntries(
|
|
1432
|
+
allowed.map((key) => {
|
|
1433
|
+
const raw = data[key];
|
|
1434
|
+
const transformed = typeof transforms[key] === "function"
|
|
1435
|
+
? transforms[key](raw, data)
|
|
1436
|
+
: raw;
|
|
1437
|
+
return [key, transformed];
|
|
1438
|
+
|
|
1439
|
+
}).filter(([_, v]) => v !== undefined)
|
|
1440
|
+
);
|
|
1441
|
+
|
|
1442
|
+
const initial = structuredClone ? structuredClone(rawDraft) : JSON.parse(JSON.stringify(rawDraft));
|
|
1443
|
+
|
|
1444
|
+
const draft = previousDraft && isReactive(previousDraft)
|
|
1445
|
+
? Object.assign(previousDraft, rawDraft)
|
|
1446
|
+
: reactive(rawDraft);
|
|
1447
|
+
|
|
1448
|
+
// Assure que serverData est réactif si c'est un objet
|
|
1449
|
+
const reactiveServer = isReactive(serverData)
|
|
1450
|
+
? serverData
|
|
1451
|
+
: (serverData && typeof serverData === "object" ? reactive(serverData) : serverData);
|
|
1452
|
+
|
|
1453
|
+
const proxy = this._createDraftProxy(apiClient, reactiveServer, draft, allowed, transforms, { throwOnError });
|
|
1454
|
+
|
|
1455
|
+
return { draft, proxy, initial};
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Invoque une méthode de bloc de manière type-safe.
|
|
1460
|
+
* Élimine le besoin de casts `as any` dans les classes dérivées.
|
|
1461
|
+
*
|
|
1462
|
+
* @param blocks - Le Map contenant les noms de méthodes (doit être `as const`)
|
|
1463
|
+
* @param methodName - Le nom de la méthode à invoquer
|
|
1464
|
+
* @param blockData - Les données à passer à la méthode
|
|
1465
|
+
* @returns La valeur retournée par la méthode
|
|
1466
|
+
* @protected
|
|
1467
|
+
*
|
|
1468
|
+
* @example
|
|
1469
|
+
* // Dans une classe dérivée :
|
|
1470
|
+
* await this._invokeBlockMethod(User.UPDATE_BLOCKS, methodName, blockData);
|
|
1471
|
+
*/
|
|
1472
|
+
protected async _invokeBlockMethod<T extends Map<string, any>>(
|
|
1473
|
+
blocks: T,
|
|
1474
|
+
methodName: string,
|
|
1475
|
+
blockData: any
|
|
1476
|
+
): Promise<any> {
|
|
1477
|
+
type MethodName = ExtractMethodNames<T>;
|
|
1478
|
+
const method = this[methodName as MethodName] as (data: any) => Promise<any>;
|
|
1479
|
+
return await method.call(this, blockData);
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* Extrait les champs modifiés du schéma.
|
|
1484
|
+
*
|
|
1485
|
+
* @param apiClient - Instance de l'API.
|
|
1486
|
+
* @param constant - Nom de la constante.
|
|
1487
|
+
* @param data - Données à comparer.
|
|
1488
|
+
* @param getInitialDraft - Fonction pour obtenir le brouillon initial.
|
|
1489
|
+
* @param removeFields - Liste des champs à ignorer.
|
|
1490
|
+
* @returns - Champs modifiés ou `null` si aucun changement.
|
|
1491
|
+
* @protected
|
|
1492
|
+
*/
|
|
1493
|
+
/**
|
|
1494
|
+
* Détecte si une valeur est un type d'upload (Buffer, File, Blob, Stream)
|
|
1495
|
+
* @param obj - La valeur à tester
|
|
1496
|
+
* @returns true si c'est un type d'upload
|
|
1497
|
+
* @private
|
|
1498
|
+
*/
|
|
1499
|
+
private _isUploadType(obj: any): boolean {
|
|
1500
|
+
// Buffer (Node.js)
|
|
1501
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(obj)) return true;
|
|
1502
|
+
// File (Browser)
|
|
1503
|
+
if (typeof File !== "undefined" && obj instanceof File) return true;
|
|
1504
|
+
// Blob (Browser)
|
|
1505
|
+
if (typeof Blob !== "undefined" && obj instanceof Blob) return true;
|
|
1506
|
+
// ReadableStream (Node.js)
|
|
1507
|
+
if (obj && typeof obj.pipe === "function" && typeof obj.on === "function") return true;
|
|
1508
|
+
return false;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
/**
|
|
1512
|
+
* Compare deux valeurs de manière intelligente :
|
|
1513
|
+
* - Types d'upload (Buffer, File, etc.) : comparaison par référence
|
|
1514
|
+
* - Autres types : comparaison JSON
|
|
1515
|
+
* @param current - Valeur actuelle
|
|
1516
|
+
* @param initial - Valeur initiale
|
|
1517
|
+
* @returns true si les valeurs sont différentes
|
|
1518
|
+
* @private
|
|
1519
|
+
*/
|
|
1520
|
+
private _compareValues(current: any, initial: any): boolean {
|
|
1521
|
+
// Si l'un des deux est un type d'upload, comparer par référence
|
|
1522
|
+
if (this._isUploadType(current) || this._isUploadType(initial)) {
|
|
1523
|
+
return current !== initial;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Sinon, comparaison JSON standard
|
|
1527
|
+
return JSON.stringify(this._toRawDeep(current)) !== JSON.stringify(this._toRawDeep(initial));
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
_extractChangedFieldsFromSchema(apiClient: ApiClient, constant: string, data: Record<string, any> = {}, getInitialDraft: () => Record<string, any> | void, removeFields: string[] = []): Record<string, any> | null {
|
|
1531
|
+
const schema = apiClient.getRequestSchema(constant);
|
|
1532
|
+
let allowed = this._extractWritableFields(schema, data);
|
|
1533
|
+
const changed: Record<string, any> = {};
|
|
1534
|
+
const initialDraft = getInitialDraft?.() || {};
|
|
1535
|
+
|
|
1536
|
+
// on enlève les champs qui ne sont pas dans le draft
|
|
1537
|
+
// ou qui sont dans removeFields
|
|
1538
|
+
allowed = allowed.filter(k => !removeFields.includes(k));
|
|
1539
|
+
|
|
1540
|
+
for (const key of allowed) {
|
|
1541
|
+
// on verifie que le champ existe dans le draft
|
|
1542
|
+
// sinon on ne le prend pas en compte
|
|
1543
|
+
|
|
1544
|
+
if (data[key] === undefined) continue;
|
|
1545
|
+
|
|
1546
|
+
const current = data[key];
|
|
1547
|
+
const initial = initialDraft[key];
|
|
1548
|
+
|
|
1549
|
+
const changedValue = this._compareValues(current, initial);
|
|
1550
|
+
|
|
1551
|
+
if (changedValue) {
|
|
1552
|
+
changed[key] = current;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
return Object.keys(changed).length > 0 ? changed : null;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
/**
|
|
1560
|
+
* Extrait tous les champs valides selon le schéma, et retourne uniquement ceux qui ont changé par rapport au draft initial.
|
|
1561
|
+
* Contrairement à `_extractChangedFieldsFromSchema`, cette méthode retourne l'ensemble des champs valides (`updated`)
|
|
1562
|
+
* uniquement s'il y a au moins un champ modifié (`changed`).
|
|
1563
|
+
*
|
|
1564
|
+
* ⚠️ Les champs sont filtrés en fonction :
|
|
1565
|
+
* - des champs définis comme modifiables dans le schéma (`writeable`)
|
|
1566
|
+
* - des champs non exclus dans `removeFields`
|
|
1567
|
+
* - des champs définis dans `data` (les `undefined` sont ignorés)
|
|
1568
|
+
*
|
|
1569
|
+
* @param apiClient - L'instance de client API contenant les schémas.
|
|
1570
|
+
* @param constant - Le nom de la constante de schéma (ex: "ADD_EVENT").
|
|
1571
|
+
* @param data - Les nouvelles données à comparer avec le draft initial.
|
|
1572
|
+
* @param getInitialDraft - Fonction qui retourne le draft initial (souvent `this.initialDraftData`).
|
|
1573
|
+
* @param removeFields - Champs à ignorer même s'ils sont valides.
|
|
1574
|
+
* @returns - Un objet `updated` avec les champs valides si au moins un champ a changé, sinon `null`.
|
|
1575
|
+
* @private
|
|
1576
|
+
*/
|
|
1577
|
+
_extractAllValidFieldsFromSchema(apiClient: ApiClient, constant: string, data: Record<string, any> = {}, getInitialDraft: () => Record<string, any>, removeFields: string[] = []): Record<string, any> | null {
|
|
1578
|
+
const schema = apiClient.getRequestSchema(constant);
|
|
1579
|
+
let allowed = this._extractWritableFields(schema, data);
|
|
1580
|
+
const changed: Record<string, any> = {};
|
|
1581
|
+
const updated: Record<string, any> = {};
|
|
1582
|
+
const initialDraft = getInitialDraft?.() || {};
|
|
1583
|
+
|
|
1584
|
+
// on enlève les champs qui ne sont pas dans le draft
|
|
1585
|
+
// ou qui sont dans removeFields
|
|
1586
|
+
allowed = allowed.filter(k => !removeFields.includes(k));
|
|
1587
|
+
|
|
1588
|
+
for (const key of allowed) {
|
|
1589
|
+
// on verifie que le champ existe dans le draft
|
|
1590
|
+
// sinon on ne le prend pas en compte
|
|
1591
|
+
|
|
1592
|
+
if (data[key] === undefined) continue;
|
|
1593
|
+
|
|
1594
|
+
const current = data[key];
|
|
1595
|
+
const initial = initialDraft[key];
|
|
1596
|
+
|
|
1597
|
+
const changedValue =
|
|
1598
|
+
JSON.stringify(current) !== JSON.stringify(initial);
|
|
1599
|
+
|
|
1600
|
+
if (changedValue) {
|
|
1601
|
+
changed[key] = current;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
updated[key] = current;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
return Object.keys(changed).length > 0 ? updated : null;
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* ───────────────────────────────
|
|
1612
|
+
* MutualEntityMixin
|
|
1613
|
+
* ───────────────────────────────
|
|
1614
|
+
*/
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Résout l'identifiant de l'entité si seul le slug est fourni.
|
|
1618
|
+
* @param type - Le type d'entité (ex : "citoyens", "organizations", "projects").
|
|
1619
|
+
* @returns L'identifiant résolu.
|
|
1620
|
+
* @throws {ApiResponseError} - Si le slug ne correspond pas à l'entité attendue.
|
|
1621
|
+
* @throws {ApiError} - Si l'identifiant ne peut pas être résolu.
|
|
1622
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de la récupération
|
|
1623
|
+
* @private
|
|
1624
|
+
*/
|
|
1625
|
+
private async _resolveId(type: string): Promise<string> {
|
|
1626
|
+
if (!this.id && this.slug) {
|
|
1627
|
+
try {
|
|
1628
|
+
const data = await this.endpointApi.getElementsKey({
|
|
1629
|
+
pathParams:{
|
|
1630
|
+
slug: this.slug
|
|
1631
|
+
}
|
|
1632
|
+
}) as GetElementsKeyResponse;
|
|
1633
|
+
|
|
1634
|
+
if(data.contextId && data.contextType === type) {
|
|
1635
|
+
this._id(data.contextId);
|
|
1636
|
+
} else {
|
|
1637
|
+
throw new ApiResponseError(`Le slug ${this.slug} ne correspond pas à un ${type}`, 200, data as object);
|
|
1638
|
+
}
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
if(error instanceof ApiResponseError) {
|
|
1641
|
+
const errorResponseData = error.responseData as Record<string, unknown>;
|
|
1642
|
+
if(errorResponseData.contextType !== type) {
|
|
1643
|
+
throw error;
|
|
1644
|
+
} else {
|
|
1645
|
+
throw new ApiResponseError(`Impossible de récupérer l'identifiant pour le slug ${this.slug}`, error.status, error.responseData);
|
|
1646
|
+
}
|
|
1647
|
+
} else {
|
|
1648
|
+
throw error;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (!this.id) {
|
|
1653
|
+
throw new ApiError(`Impossible de résoudre l'identifiant pour le type ${type}.`, 404);
|
|
1654
|
+
}
|
|
1655
|
+
return this.id;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
/**
|
|
1659
|
+
* Récupère le profil public de l'entité.
|
|
1660
|
+
*
|
|
1661
|
+
* @returns - Les données du profil public.
|
|
1662
|
+
* @protected
|
|
1663
|
+
*/
|
|
1664
|
+
protected async _getPublicProfile(): Promise<Record<string, any>> {
|
|
1665
|
+
await this._resolveId(this.getEntityType());
|
|
1666
|
+
if (!this.id) {
|
|
1667
|
+
throw new ApiError("L'identifiant de l'entité n'est pas défini.", 400);
|
|
1668
|
+
}
|
|
1669
|
+
const type = this.getEntityType();
|
|
1670
|
+
|
|
1671
|
+
if (type === "news" || type === "comments") {
|
|
1672
|
+
throw new ApiError("getElementsAbout ne supporte pas le type 'news'.", 400);
|
|
1673
|
+
}
|
|
1674
|
+
return this.endpointApi.getElementsAbout({ pathParams: { id: this.id, type: type }, tpl: "ficheInfoElement" });
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* Récupère la classe d'entité et ses dépendances à partir du type d'entité.
|
|
1679
|
+
*
|
|
1680
|
+
* @private
|
|
1681
|
+
*/
|
|
1682
|
+
private _getEntityMeta(entityType: string): EntityMeta | null {
|
|
1683
|
+
const selfClass = this.constructor;
|
|
1684
|
+
const selfTag = this.__entityTag;
|
|
1685
|
+
|
|
1686
|
+
const commonDeps = {
|
|
1687
|
+
EndpointApi: this.deps.EndpointApi,
|
|
1688
|
+
User: selfTag === "User" ? selfClass : this.deps.User,
|
|
1689
|
+
Organization: selfTag === "Organization" ? selfClass : this.deps.Organization,
|
|
1690
|
+
Project: selfTag === "Project" ? selfClass : this.deps.Project,
|
|
1691
|
+
Event: selfTag === "Event" ? selfClass : this.deps.Event,
|
|
1692
|
+
Poi: selfTag === "Poi" ? selfClass : this.deps.Poi,
|
|
1693
|
+
Badge: selfTag === "Badge" ? selfClass : this.deps.Badge,
|
|
1694
|
+
News: selfTag === "News" ? selfClass : this.deps.News,
|
|
1695
|
+
Comment: selfTag === "Comment" ? selfClass : this.deps.Comment,
|
|
1696
|
+
};
|
|
1697
|
+
|
|
1698
|
+
const map = {
|
|
1699
|
+
citoyens: { entityClass: commonDeps.User, deps: commonDeps },
|
|
1700
|
+
organizations:{ entityClass: commonDeps.Organization, deps: commonDeps },
|
|
1701
|
+
projects: { entityClass: commonDeps.Project, deps: commonDeps },
|
|
1702
|
+
events: { entityClass: commonDeps.Event, deps: { ...commonDeps, Badge: undefined } },
|
|
1703
|
+
poi: { entityClass: commonDeps.Poi, deps: { ...commonDeps, Badge: undefined, News: undefined } },
|
|
1704
|
+
news: { entityClass: commonDeps.News, deps: { ...commonDeps } },
|
|
1705
|
+
badges: { entityClass: commonDeps.Badge, deps: {
|
|
1706
|
+
EndpointApi: commonDeps.EndpointApi,
|
|
1707
|
+
User: commonDeps.User,
|
|
1708
|
+
Organization: commonDeps.Organization,
|
|
1709
|
+
Project: commonDeps.Project
|
|
1710
|
+
} },
|
|
1711
|
+
comments: { entityClass: commonDeps.Comment, deps: { ...commonDeps } },
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
return (map as Record<string, EntityMeta | undefined>)[entityType] || null;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
/**
|
|
1718
|
+
* Lier des données d'entité à une instance d'entité.
|
|
1719
|
+
*
|
|
1720
|
+
* @param entityType - Le type d'entité (ex : "citoyens", "organisations", "projets").
|
|
1721
|
+
* @param entityData - Les données de l'entité à lier.
|
|
1722
|
+
* @return L'entité liée ou les données brutes si la metadata n'existe pas.
|
|
1723
|
+
* @private
|
|
1724
|
+
*/
|
|
1725
|
+
_linkEntity(entityType: string, entityData: object): AnyEntity | object {
|
|
1726
|
+
const meta = this._getEntityMeta(entityType);
|
|
1727
|
+
if (!meta) return entityData;
|
|
1728
|
+
return meta.entityClass.fromServerData(entityData, this, meta.deps);
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Transforme un objet imbriqué avec {id, type} en instance d'entité.
|
|
1733
|
+
* Utilisé pour transformer des champs comme author, target, etc.
|
|
1734
|
+
*
|
|
1735
|
+
* @param obj - L'objet à transformer (doit avoir au minimum {id, type}).
|
|
1736
|
+
* @returns L'instance d'entité ou l'objet original si la transformation échoue.
|
|
1737
|
+
* @protected
|
|
1738
|
+
*/
|
|
1739
|
+
protected _linkNestedEntity<T = any>(obj: any): T | any {
|
|
1740
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
1741
|
+
if (!obj.id || !obj.type) return obj;
|
|
1742
|
+
|
|
1743
|
+
try {
|
|
1744
|
+
return this._linkEntity(obj.type, obj) as T;
|
|
1745
|
+
} catch (error) {
|
|
1746
|
+
this.apiClient._logger?.warn?.(`Impossible de lier l'entité imbriquée de type ${obj.type}:`, error);
|
|
1747
|
+
return obj;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/**
|
|
1752
|
+
* Récupère et lie une entité à partir de son ID.
|
|
1753
|
+
*
|
|
1754
|
+
* @param entityType - Le type d'entité.
|
|
1755
|
+
* @param entityId - L'identifiant de l'entité.
|
|
1756
|
+
* @param options - Options supplémentaires :
|
|
1757
|
+
* @param options.skipGet - Si true, ne pas appeler `get()` sur l'entité.
|
|
1758
|
+
* @return L'entité liée ou null.
|
|
1759
|
+
* @private
|
|
1760
|
+
*/
|
|
1761
|
+
async _linkEntityById(entityType: string, entityId: string, options?: { skipGet?: boolean }): Promise<AnyEntity|null> {
|
|
1762
|
+
const meta = this._getEntityMeta(entityType);
|
|
1763
|
+
if (!meta) return null;
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
const parent: ParentLike = this as ParentLike;
|
|
1767
|
+
|
|
1768
|
+
const entity = new meta.entityClass(parent, { id: entityId }, meta.deps) as AnyEntity;
|
|
1769
|
+
if (options?.skipGet) return entity;
|
|
1770
|
+
await entity.get();
|
|
1771
|
+
return entity;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Lie une liste d'entités à partir de leurs données.
|
|
1776
|
+
*
|
|
1777
|
+
* @param results - Liste de données d'entités.
|
|
1778
|
+
* @return Liste d'entités liées.
|
|
1779
|
+
* @private
|
|
1780
|
+
*/
|
|
1781
|
+
protected _linkEntities(results: any[]): any[] {
|
|
1782
|
+
return results.flatMap((d: any) => {
|
|
1783
|
+
if (!d?.collection) {
|
|
1784
|
+
this.apiClient._logger?.warn?.(`Objet ignoré car sans 'collection' : ${d?.id}`);
|
|
1785
|
+
return []; // exclu de la liste
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
return [this._linkEntity?.(d.collection, d) ?? d];
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* Lie des entités présentes dans `this.serverData` à partir de leurs IDs,
|
|
1794
|
+
* en les filtrant dynamiquement et en optionnellement les transformant.
|
|
1795
|
+
*
|
|
1796
|
+
* @param entityType - Le type d'entité (ex : "badges", "citoyens", etc.).
|
|
1797
|
+
* @param filters - Clés/valeurs de filtres dynamiques. Les valeurs peuvent être :
|
|
1798
|
+
* - un littéral (comparaison stricte ou intelligente selon le type),
|
|
1799
|
+
* - une chaîne (utilise `includes` insensible à la casse),
|
|
1800
|
+
* - une RegExp (appliquée si la valeur est une chaîne),
|
|
1801
|
+
* - une fonction `(value) => boolean`.
|
|
1802
|
+
* @param options - Options supplémentaires :
|
|
1803
|
+
* @param options.key - Le champ de `this.serverData` à utiliser (par défaut = entityType).
|
|
1804
|
+
* @param options.mapFn - Fonction de transformation `(entity) => any` appliquée au résultat.
|
|
1805
|
+
*
|
|
1806
|
+
* @return Liste des entités liées, filtrées et éventuellement transformées.
|
|
1807
|
+
*
|
|
1808
|
+
* @example
|
|
1809
|
+
* // Tous les badges avec `name` contenant "codev"
|
|
1810
|
+
* const badges = await this.linkEntitiesFromServerData("badges", { name: "codev" });
|
|
1811
|
+
*
|
|
1812
|
+
* @example
|
|
1813
|
+
* // Badges non expirés et visibles
|
|
1814
|
+
* const badges = await this.linkEntitiesFromServerData("badges", {
|
|
1815
|
+
* expiredOn: false,
|
|
1816
|
+
* show: "true"
|
|
1817
|
+
* });
|
|
1818
|
+
*
|
|
1819
|
+
* @example
|
|
1820
|
+
* // Badges émis après 2023
|
|
1821
|
+
* const badges = await this.linkEntitiesFromServerData("badges", {
|
|
1822
|
+
* issuedOn: (v) => new Date(v) >= new Date("2023-01-01")
|
|
1823
|
+
* });
|
|
1824
|
+
*
|
|
1825
|
+
* @example
|
|
1826
|
+
* // Extraire uniquement les noms des badges
|
|
1827
|
+
* const namesOnly = await this.linkEntitiesFromServerData("badges", {}, {
|
|
1828
|
+
* mapFn: (badge) => badge.meta.name
|
|
1829
|
+
* });
|
|
1830
|
+
*/
|
|
1831
|
+
async linkEntitiesFromServerData(entityType: string, filters: EntityFilters = {}, options: LinkEntitiesOptions = {}): Promise<any[]> {
|
|
1832
|
+
const key = options.key || entityType;
|
|
1833
|
+
const mapFn = typeof options.mapFn === "function" ? options.mapFn : null;
|
|
1834
|
+
|
|
1835
|
+
const isTruthy = (v: any) => v === true || v === "true";
|
|
1836
|
+
const isFalsy = (v: any) => v === false || v === "false";
|
|
1837
|
+
|
|
1838
|
+
const data = (this.serverData as Record<string, any>)?.[key];
|
|
1839
|
+
const result: any[] = [];
|
|
1840
|
+
|
|
1841
|
+
if (data && typeof data === "object" && Object.keys(data).length > 0) {
|
|
1842
|
+
for (const id of Object.keys(data)) {
|
|
1843
|
+
const meta = data[id];
|
|
1844
|
+
|
|
1845
|
+
const matches = Object.entries(filters).every(([key, expected]) => {
|
|
1846
|
+
const actual = meta[key];
|
|
1847
|
+
|
|
1848
|
+
if (typeof expected === "function") {
|
|
1849
|
+
try {
|
|
1850
|
+
return expected(actual);
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
console.warn(`Erreur dans le filtre personnalisé pour ${key}`, err);
|
|
1853
|
+
return false;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
if (expected instanceof RegExp) {
|
|
1858
|
+
return typeof actual === "string" && expected.test(actual);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (typeof expected === "string" && typeof actual === "string") {
|
|
1862
|
+
return actual.toLowerCase().includes(expected.toLowerCase());
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (isTruthy(expected)) return isTruthy(actual);
|
|
1866
|
+
if (isFalsy(expected)) return isFalsy(actual);
|
|
1867
|
+
|
|
1868
|
+
if (
|
|
1869
|
+
typeof actual === "string" &&
|
|
1870
|
+
typeof expected === "string" &&
|
|
1871
|
+
!isNaN(Date.parse(actual)) &&
|
|
1872
|
+
!isNaN(Date.parse(expected))
|
|
1873
|
+
) {
|
|
1874
|
+
return new Date(actual).getTime() === new Date(expected).getTime();
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
return actual === expected;
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
if (!matches) continue;
|
|
1881
|
+
|
|
1882
|
+
try {
|
|
1883
|
+
const entity = await this._linkEntityById(entityType, id);
|
|
1884
|
+
if (entity) {
|
|
1885
|
+
(entity as AnyEntity & { meta: any }).meta = meta;
|
|
1886
|
+
result.push(mapFn ? mapFn(entity) : entity);
|
|
1887
|
+
}
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
this.apiClient._logger?.error?.(`Erreur lors de la récupération de l'entité ${entityType} (${id}) :`, error);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return result;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/**
|
|
1897
|
+
* Crée une instance d'entité à partir des données fournies.
|
|
1898
|
+
* @template keyof EntityTypeMap T
|
|
1899
|
+
* @param entityType
|
|
1900
|
+
* @param entityData
|
|
1901
|
+
* @return {EntityTypeMap[T]}
|
|
1902
|
+
* @private
|
|
1903
|
+
*/
|
|
1904
|
+
_entityInstanceData<T extends keyof EntityTypeMap>(entityType: T, entityData: object): EntityTypeMap[T] {
|
|
1905
|
+
const meta = this._getEntityMeta(entityType);
|
|
1906
|
+
if (!meta) {
|
|
1907
|
+
throw new ApiError(`Type d'entité inconnu: ${entityType}`, 400);
|
|
1908
|
+
}
|
|
1909
|
+
const selfKey = this.__entityTag; // ex: "User", "Organization", etc.
|
|
1910
|
+
const selfClass = this.constructor;
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
const parent: ParentLike = this as ParentLike;
|
|
1914
|
+
|
|
1915
|
+
// pour citoyens la signature est différentes
|
|
1916
|
+
return new meta.entityClass(parent, entityData, {
|
|
1917
|
+
[selfKey]: selfClass,
|
|
1918
|
+
...meta.deps
|
|
1919
|
+
}) as EntityTypeMap[T];
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/**
|
|
1923
|
+
* Crée une instance d'entité, et déclenche get() si certaines propriétés sont présentes.
|
|
1924
|
+
* @template keyof EntityTypeMap T
|
|
1925
|
+
* @param entityType
|
|
1926
|
+
* @param [entityData={}]
|
|
1927
|
+
* @return {Promise<EntityTypeMap[T]>}
|
|
1928
|
+
*/
|
|
1929
|
+
async entity<T extends keyof EntityTypeMap>(entityType: T, entityData: object = {}): Promise<EntityTypeMap[T]> {
|
|
1930
|
+
try {
|
|
1931
|
+
const entity = this._entityInstanceData(entityType, entityData);
|
|
1932
|
+
|
|
1933
|
+
const fetchKeysByEntity = {
|
|
1934
|
+
citoyens: ["id", "slug"],
|
|
1935
|
+
organizations: ["id", "slug"],
|
|
1936
|
+
projects: ["id", "slug"],
|
|
1937
|
+
events: ["id", "slug"],
|
|
1938
|
+
poi: ["id", "slug"],
|
|
1939
|
+
news: ["id"],
|
|
1940
|
+
badges: ["id"],
|
|
1941
|
+
comments: [], // Pas de get() pour les commentaires
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1944
|
+
const fetchKeys = (fetchKeysByEntity as Record<string, string[] | undefined>)[entityType];
|
|
1945
|
+
|
|
1946
|
+
if (fetchKeys && this._hasAtLeastOne(entityData, fetchKeys)) {
|
|
1947
|
+
await entity.get();
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
return entity;
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
this.apiClient._logger.error(`[Api.${this.__entityTag}.${entityType}] Erreur lors de la création d'une instance ${entityType} :`, (error as Error).message);
|
|
1953
|
+
throw error;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
/**
|
|
1958
|
+
* Récupère et lie une entité à partir de son slug.
|
|
1959
|
+
* @param slug - Le slug de l'entité.
|
|
1960
|
+
* @return L'entité liée.
|
|
1961
|
+
* @throws {ApiError} Si le slug est vide.
|
|
1962
|
+
* @throws {ApiResponseError} Si le slug n'existe pas ou est invalide.
|
|
1963
|
+
*/
|
|
1964
|
+
async entityBySlug(slug: string): Promise<AnyEntity> {
|
|
1965
|
+
if (!slug) {
|
|
1966
|
+
throw new ApiError("Le slug est requis.", 400);
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
const data = await this.endpointApi.getElementsKey({
|
|
1970
|
+
pathParams:{
|
|
1971
|
+
slug: slug
|
|
1972
|
+
}
|
|
1973
|
+
}) as GetElementsKeyResponse;
|
|
1974
|
+
|
|
1975
|
+
if(data.contextId && data.contextType) {
|
|
1976
|
+
const entity = await this.entity(data.contextType, { id: data.contextId });
|
|
1977
|
+
return entity;
|
|
1978
|
+
} else {
|
|
1979
|
+
throw new ApiResponseError(`Le slug ${slug} n'est pas valide`, 200, data as object);
|
|
1980
|
+
}
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
if(error instanceof ApiResponseError) {
|
|
1983
|
+
throw new ApiResponseError(`Impossible de récupérer l'identifiant pour le slug ${slug}`, error.status, error.responseData);
|
|
1984
|
+
} else {
|
|
1985
|
+
throw error;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
/**
|
|
1991
|
+
* Construit dynamiquement des filtres sur un lien entre entités
|
|
1992
|
+
*
|
|
1993
|
+
* @param id - L'ID de l'entité cible.
|
|
1994
|
+
* @param options - Options de filtrage.
|
|
1995
|
+
* @param options.linkType - Le type de lien (ex: "memberOf", "projects", etc.).
|
|
1996
|
+
* @param options.isAdmin - Si défini, filtre selon l'existence de isAdmin.
|
|
1997
|
+
* @param options.isAdminPending - Si défini, filtre selon l'existence de isAdminPending.
|
|
1998
|
+
* @param options.isInviting - Si défini, filtre selon l'existence de isInviting.
|
|
1999
|
+
* @param options.toBeValidated - Si défini, filtre selon l'existence de toBeValidated.
|
|
2000
|
+
* @param options.roles - Rôles à filtrer.
|
|
2001
|
+
* @returns - Un objet de filtres à passer à une requête.
|
|
2002
|
+
* @throws {TypeError} - Si les types des paramètres ne sont pas valides.
|
|
2003
|
+
* @protected
|
|
2004
|
+
*/
|
|
2005
|
+
_buildLinkFilters(id: string | null, {
|
|
2006
|
+
linkType,
|
|
2007
|
+
isAdmin,
|
|
2008
|
+
isAdminPending,
|
|
2009
|
+
isInviting,
|
|
2010
|
+
toBeValidated = false,
|
|
2011
|
+
roles = []
|
|
2012
|
+
}: {
|
|
2013
|
+
linkType: "memberOf" | "projects" | "events";
|
|
2014
|
+
isAdmin?: boolean;
|
|
2015
|
+
isAdminPending?: boolean;
|
|
2016
|
+
isInviting?: boolean;
|
|
2017
|
+
toBeValidated?: boolean;
|
|
2018
|
+
roles?: string[];
|
|
2019
|
+
}): LinkFilters {
|
|
2020
|
+
if (typeof id !== "string" || id === null) throw new TypeError("id doit être une chaîne non-nulle.");
|
|
2021
|
+
if (typeof linkType !== "string") throw new TypeError("linkType doit être une chaîne.");
|
|
2022
|
+
if (typeof isAdmin !== "undefined" && typeof isAdmin !== "boolean") {
|
|
2023
|
+
throw new TypeError("isAdmin doit être un booléen.");
|
|
2024
|
+
}
|
|
2025
|
+
if (typeof isAdminPending !== "undefined" && typeof isAdminPending !== "boolean") {
|
|
2026
|
+
throw new TypeError("isAdminPending doit être un booléen.");
|
|
2027
|
+
}
|
|
2028
|
+
if (typeof isInviting !== "undefined" && typeof isInviting !== "boolean") {
|
|
2029
|
+
throw new TypeError("isInviting doit être un booléen.");
|
|
2030
|
+
}
|
|
2031
|
+
if (typeof toBeValidated !== "undefined" && typeof toBeValidated !== "boolean") {
|
|
2032
|
+
throw new TypeError("toBeValidated doit être un booléen.");
|
|
2033
|
+
}
|
|
2034
|
+
if (!Array.isArray(roles)) {
|
|
2035
|
+
throw new TypeError("roles doit être un tableau de chaînes.");
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
const path = `links.${linkType}.${id}`;
|
|
2039
|
+
|
|
2040
|
+
const filters: LinkFilters = {
|
|
2041
|
+
[`${path}`]: { $exists: true }
|
|
2042
|
+
};
|
|
2043
|
+
|
|
2044
|
+
if (typeof toBeValidated === "boolean") {
|
|
2045
|
+
filters[`${path}.toBeValidated`] = { $exists: toBeValidated };
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
if (typeof isInviting === "boolean") {
|
|
2049
|
+
filters[`${path}.isInviting`] = { $exists: isInviting };
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
if (isAdmin === true) {
|
|
2053
|
+
filters[`${path}.isAdmin`] = { $exists: true };
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
if(isAdminPending === true) {
|
|
2057
|
+
filters[`${path}.isAdminPending`] = { $exists: true };
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
if (roles.length > 0) {
|
|
2061
|
+
filters[`${path}.roles`] = { $in: roles };
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
return filters;
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
/**
|
|
2068
|
+
* Retourne les métadonnées de lien pour une entité :
|
|
2069
|
+
* - `linkType` : clé dans `serverData.links`
|
|
2070
|
+
* - `connectTypeConnect` : valeur envoyée pour `connect()`
|
|
2071
|
+
* - `connectTypeDisconnect` : valeur envoyée pour `disconnect()`
|
|
2072
|
+
* @throws {ApiError} - Si le type d'entité est inconnu.
|
|
2073
|
+
* @protected
|
|
2074
|
+
*/
|
|
2075
|
+
protected _getLinkMeta(): LinkMeta {
|
|
2076
|
+
const map = {
|
|
2077
|
+
organizations: {
|
|
2078
|
+
linkType: "memberOf",
|
|
2079
|
+
connectTypeConnect: "member",
|
|
2080
|
+
connectTypeDisconnect: "members"
|
|
2081
|
+
},
|
|
2082
|
+
projects: {
|
|
2083
|
+
linkType: "projects",
|
|
2084
|
+
connectTypeConnect: "contributor",
|
|
2085
|
+
connectTypeDisconnect: "contributors"
|
|
2086
|
+
},
|
|
2087
|
+
events: {
|
|
2088
|
+
linkType: "events",
|
|
2089
|
+
connectTypeConnect: "attendee",
|
|
2090
|
+
connectTypeDisconnect: "attendees"
|
|
2091
|
+
},
|
|
2092
|
+
citoyens: {
|
|
2093
|
+
linkType: "friends",
|
|
2094
|
+
connectTypeConnect: "friend",
|
|
2095
|
+
connectTypeDisconnect: "friends"
|
|
2096
|
+
}
|
|
2097
|
+
};
|
|
2098
|
+
|
|
2099
|
+
const entityType = this.getEntityType();
|
|
2100
|
+
const meta = (map as Record<string, LinkMeta | undefined>)[entityType];
|
|
2101
|
+
|
|
2102
|
+
if (!meta) {
|
|
2103
|
+
throw new ApiError(`Aucune correspondance de lien définie pour le type d'entité "${entityType}"`, 404);
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
return meta;
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/**
|
|
2110
|
+
* Vérifie si l'entité prend en charge les opérations de lien (`connect`, `disconnect`, etc.).
|
|
2111
|
+
* Utilise `_getLinkMeta()` comme source unique de vérité.
|
|
2112
|
+
*
|
|
2113
|
+
* @throws {ApiError} - Si l'entité ne supporte pas les liens.
|
|
2114
|
+
* @protected
|
|
2115
|
+
*/
|
|
2116
|
+
protected _checkLinkableEntity() {
|
|
2117
|
+
try {
|
|
2118
|
+
this._getLinkMeta(); // si l'entité n'est pas supportée, ça throw déjà
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
if (error instanceof ApiError) {
|
|
2121
|
+
throw new ApiError(`L'entité "${this.getEntityType()}" ne prend pas en charge les actions de lien.`, 400);
|
|
2122
|
+
}
|
|
2123
|
+
throw error;
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
/**
|
|
2128
|
+
* Retourne le lien de l'utilisateur connecté avec l'entité cible (dans `parent.serverData.links`)
|
|
2129
|
+
* @protected
|
|
2130
|
+
*/
|
|
2131
|
+
protected _getLinkFromConnectedUser() {
|
|
2132
|
+
const { linkType } = this._getLinkMeta();
|
|
2133
|
+
if(!this.id) {
|
|
2134
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2135
|
+
}
|
|
2136
|
+
return this?.userContext?.serverData?.links?.[linkType]?.[this.id] || null;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/**
|
|
2140
|
+
* Soumet une demande de connexion à une entité (ex : membre, contributeur),
|
|
2141
|
+
* ou valide l'invitation si elle existe déjà.
|
|
2142
|
+
*
|
|
2143
|
+
* @returns - Résultat de l'API
|
|
2144
|
+
* @throws {ApiError} - En cas d'erreur de connexion ou d'invitation.
|
|
2145
|
+
* @private
|
|
2146
|
+
*/
|
|
2147
|
+
private async _submitLinkRequest(): Promise<unknown> {
|
|
2148
|
+
|
|
2149
|
+
if (!this.id) {
|
|
2150
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if(!this.userId) {
|
|
2154
|
+
throw new ApiError("L'utilisateur connecté n'est pas défini.", 400);
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const { connectTypeConnect } = this._getLinkMeta();
|
|
2158
|
+
|
|
2159
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
2160
|
+
|
|
2161
|
+
// Cas : aucun lien → on demande à se connecter
|
|
2162
|
+
if (!userLink) {
|
|
2163
|
+
|
|
2164
|
+
const t = this.getEntityType();
|
|
2165
|
+
|
|
2166
|
+
// 1) Garde runtime pour exclure les types non pris en charge par ConnectData
|
|
2167
|
+
if (t === "poi" || t === "badges" || t === "news") {
|
|
2168
|
+
throw new ApiError(`Le type d'entité "${t}" ne supporte pas cette connexion.`, 400);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// 2) Narrow de type pour TypeScript
|
|
2172
|
+
const parentType = t as ConnectData["parentType"];
|
|
2173
|
+
|
|
2174
|
+
const data: ConnectData = {
|
|
2175
|
+
childId: this.userId,
|
|
2176
|
+
childType: "citoyens",
|
|
2177
|
+
parentType,
|
|
2178
|
+
parentId: this.id,
|
|
2179
|
+
connectType: connectTypeConnect
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
// TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
|
|
2183
|
+
const retour = await this.callIsMe(() => this.endpointApi.connect(data));
|
|
2184
|
+
await this.userContext?.refresh();
|
|
2185
|
+
return retour;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// Cas : invitation reçue → on valide l'invitation
|
|
2189
|
+
if (userLink.isInviting) {
|
|
2190
|
+
return this._acceptLinkRequest();
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
// Cas : déjà en attente
|
|
2194
|
+
if (userLink.toBeValidated) {
|
|
2195
|
+
throw new ApiError("Vous êtes déjà en attente de validation.", 400);
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
// Cas par défaut : rien à faire
|
|
2199
|
+
throw new ApiError("Vous êtes déjà connecté à cette entité.", 400);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Soumet une demande pour devenir administrateur d'une entité.
|
|
2204
|
+
*
|
|
2205
|
+
* @returns {Promise<Object>}
|
|
2206
|
+
* @throws {ApiError}
|
|
2207
|
+
* @private
|
|
2208
|
+
*/
|
|
2209
|
+
private async _submitLinkRequestAdmin(): Promise<unknown> {
|
|
2210
|
+
|
|
2211
|
+
if (!this.id) {
|
|
2212
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
if (!this.userId) {
|
|
2216
|
+
throw new ApiError("L'utilisateur connecté n'est pas défini.", 400);
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
2220
|
+
|
|
2221
|
+
// Aucun lien existant → envoie une demande avec rôle "admin"
|
|
2222
|
+
if (!userLink) {
|
|
2223
|
+
|
|
2224
|
+
const t = this.getEntityType();
|
|
2225
|
+
|
|
2226
|
+
// 1) Garde runtime pour exclure les types non pris en charge par ConnectData
|
|
2227
|
+
if (t === "poi" || t === "badges" || t === "news") {
|
|
2228
|
+
throw new ApiError(`Le type d'entité "${t}" ne supporte pas cette connexion.`, 400);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// 2) Narrow de type pour TypeScript
|
|
2232
|
+
const parentType = t as ConnectData["parentType"];
|
|
2233
|
+
|
|
2234
|
+
const data: ConnectData = {
|
|
2235
|
+
childId: this.userId,
|
|
2236
|
+
childType: "citoyens",
|
|
2237
|
+
parentType,
|
|
2238
|
+
parentId: this.id,
|
|
2239
|
+
connectType: "admin"
|
|
2240
|
+
};
|
|
2241
|
+
|
|
2242
|
+
// TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
|
|
2243
|
+
const retour = await this.callIsMe(() => this.endpointApi.connect(data));
|
|
2244
|
+
await this.userContext?.refresh();
|
|
2245
|
+
return retour;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// Invitation reçue → accepte automatiquement
|
|
2249
|
+
if (userLink.isInviting) {
|
|
2250
|
+
return this._acceptLinkRequest();
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
// Déjà en attente pour admin
|
|
2254
|
+
if (userLink.toBeValidated && userLink.isAdminPending) {
|
|
2255
|
+
throw new ApiError("Vous êtes déjà en attente de validation pour devenir admin.", 400);
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// Déjà en attente pour un autre rôle
|
|
2259
|
+
if (userLink.toBeValidated) {
|
|
2260
|
+
throw new ApiError("Vous êtes déjà en attente de validation pour rejoindre cette entité.", 400);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// Déjà connecté
|
|
2264
|
+
throw new ApiError("Vous êtes déjà connecté à cette entité.", 400);
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
/**
|
|
2268
|
+
* Accepte une demande de lien (ex : invitation à rejoindre un groupe).
|
|
2269
|
+
*
|
|
2270
|
+
* @returns - Résultat de l'API
|
|
2271
|
+
* @throws {ApiError} - En cas d'erreur de connexion ou d'invitation.
|
|
2272
|
+
* @private
|
|
2273
|
+
*/
|
|
2274
|
+
private async _acceptLinkRequest(): Promise<unknown> {
|
|
2275
|
+
|
|
2276
|
+
if (!this.id) {
|
|
2277
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
if (!this.userId) {
|
|
2281
|
+
throw new ApiError("L'utilisateur connecté n'est pas défini.", 400);
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
2285
|
+
|
|
2286
|
+
if (userLink && userLink.isInviting) {
|
|
2287
|
+
const t = this.getEntityType();
|
|
2288
|
+
|
|
2289
|
+
// 1) Garde runtime pour exclure les types non pris en charge par LinkValidateData
|
|
2290
|
+
if (t === "poi" || t === "badges" || t === "news") {
|
|
2291
|
+
throw new ApiError(`Le type d'entité "${t}" ne supporte pas cette connexion.`, 400);
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// 2) Narrow de type pour TypeScript
|
|
2295
|
+
const parentType = t as LinkValidateData["parentType"];
|
|
2296
|
+
|
|
2297
|
+
const data: LinkValidateData = {
|
|
2298
|
+
childId: this.userId,
|
|
2299
|
+
childType: "citoyens",
|
|
2300
|
+
parentType,
|
|
2301
|
+
parentId: this.id,
|
|
2302
|
+
linkOption: "isInviting"
|
|
2303
|
+
};
|
|
2304
|
+
// TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
|
|
2305
|
+
const retour = await this.callIsMe(() => this.endpointApi.linkValidate(data));
|
|
2306
|
+
await this.userContext?.refresh();
|
|
2307
|
+
return retour;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
throw new ApiError("Vous n'avez pas d'invitation à valider.", 400);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/**
|
|
2314
|
+
* Annule une demande de lien ou se déconnecte d'une entité.
|
|
2315
|
+
*
|
|
2316
|
+
* @returns - Résultat de l'API
|
|
2317
|
+
* @throws {ApiError} - En cas d'erreur de connexion ou d'invitation.
|
|
2318
|
+
* @private
|
|
2319
|
+
*/
|
|
2320
|
+
private async _leaveLinkRequest(): Promise<unknown> {
|
|
2321
|
+
|
|
2322
|
+
if (!this.id) {
|
|
2323
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
if (!this.userId) {
|
|
2327
|
+
throw new ApiError("L'utilisateur connecté n'est pas défini.", 400);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
const { connectTypeDisconnect } = this._getLinkMeta();
|
|
2331
|
+
|
|
2332
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
2333
|
+
|
|
2334
|
+
if (!userLink) {
|
|
2335
|
+
throw new ApiError("Vous n'êtes pas connecté à cette entité.", 404);
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
const t = this.getEntityType();
|
|
2339
|
+
|
|
2340
|
+
// 1) Garde runtime pour exclure les types non pris en charge par DisconnectData
|
|
2341
|
+
if (t === "poi" || t === "badges" || t === "news") {
|
|
2342
|
+
throw new ApiError(`Le type d'entité "${t}" ne supporte pas cette connexion.`, 400);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// 2) Narrow de type pour TypeScript
|
|
2346
|
+
const parentType = t as DisconnectData["parentType"];
|
|
2347
|
+
|
|
2348
|
+
const data: DisconnectData = {
|
|
2349
|
+
childId: this.userId,
|
|
2350
|
+
childType: "citoyens",
|
|
2351
|
+
parentType,
|
|
2352
|
+
parentId: this.id,
|
|
2353
|
+
// Normalement en auto dans le schema mais je le met quand même
|
|
2354
|
+
connectType: connectTypeDisconnect
|
|
2355
|
+
};
|
|
2356
|
+
|
|
2357
|
+
// TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
|
|
2358
|
+
const retour = await this.callIsMe(() => this.endpointApi.disconnect(data));
|
|
2359
|
+
await this.userContext?.refresh();
|
|
2360
|
+
return retour;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/**
|
|
2364
|
+
* ───────────────────────────────
|
|
2365
|
+
* EntityMixin
|
|
2366
|
+
* ───────────────────────────────
|
|
2367
|
+
*/
|
|
2368
|
+
|
|
2369
|
+
/**
|
|
2370
|
+
* Récupérer le profil public de l'entité
|
|
2371
|
+
*
|
|
2372
|
+
* @returns - Les données de réponse.
|
|
2373
|
+
*/
|
|
2374
|
+
async get(): Promise<Record<string, any>> {
|
|
2375
|
+
return this.apiClient.safeCall(async () => {
|
|
2376
|
+
const data = await this._getPublicProfile();
|
|
2377
|
+
this._setData(data as TServerData, { forceInitialDraftReset: true });
|
|
2378
|
+
return data;
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
/**
|
|
2383
|
+
* Mettre à jour les paramètres d'un élément : Mise à jour des paramètres spécifiques d'un élément.
|
|
2384
|
+
* Constant : UPDATE_SETTINGS
|
|
2385
|
+
* @param data
|
|
2386
|
+
* @returns - Les données de réponse.
|
|
2387
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2388
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
2389
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2390
|
+
*/
|
|
2391
|
+
async updateSettings(data: Omit<UpdateSettingsData, "idEntity" | "typeEntity">): Promise<unknown> {
|
|
2392
|
+
|
|
2393
|
+
if (!this.id) {
|
|
2394
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// Réduit le type de l'entité aux seuls autorisés par UpdateSettingsData
|
|
2398
|
+
const t = this.getEntityType();
|
|
2399
|
+
if (t === "poi" || t === "badges" || t === "news") {
|
|
2400
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_SETTINGS.`, 400);
|
|
2401
|
+
}
|
|
2402
|
+
const typeEntity = t as UpdateSettingsData["typeEntity"];
|
|
2403
|
+
|
|
2404
|
+
// Garde runtime pour s'assurer que 'type' et 'value' sont bien fournis
|
|
2405
|
+
const { type, value } = data;
|
|
2406
|
+
if (typeof type !== "string") {
|
|
2407
|
+
throw new ApiValidationError("Le champ 'type' est requis (string).", 400, ["type requis"]);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const payload: UpdateSettingsData = {
|
|
2411
|
+
type,
|
|
2412
|
+
value,
|
|
2413
|
+
idEntity: this.id,
|
|
2414
|
+
typeEntity
|
|
2415
|
+
};
|
|
2416
|
+
|
|
2417
|
+
return this.callIsConnected(() => this.endpointApi.updateSettings(payload));
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* Mettre à jour la description d'un élément : Permet de mettre à jour la description courte et complète d'un élément.
|
|
2422
|
+
* Constant : UPDATE_BLOCK_DESCRIPTION
|
|
2423
|
+
* @param data - Les données à envoyer.
|
|
2424
|
+
* @returns - Les données de réponse.
|
|
2425
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2426
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
2427
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2428
|
+
*/
|
|
2429
|
+
async updateDescription(data: Omit<UpdateBlockDescriptionData, "id" | "typeElement" | "block">): Promise<unknown> {
|
|
2430
|
+
|
|
2431
|
+
if (!this.id) {
|
|
2432
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// Réduit le type de l'entité aux seuls autorisés par UpdateBlockDescriptionData
|
|
2436
|
+
const t = this.getEntityType();
|
|
2437
|
+
if (t === "badges" || t === "news") {
|
|
2438
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_DESCRIPTION.`, 400);
|
|
2439
|
+
}
|
|
2440
|
+
const typeElement = t as UpdateBlockDescriptionData["typeElement"];
|
|
2441
|
+
|
|
2442
|
+
const payload: UpdateBlockDescriptionData = {
|
|
2443
|
+
block: "descriptions",
|
|
2444
|
+
id: this.id,
|
|
2445
|
+
typeElement,
|
|
2446
|
+
...data
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockDescription(payload));
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
/**
|
|
2453
|
+
* Mettre à jour les informations d'un élément : Permet de mettre à jour les informations générales d'un élément (nom, contacts, etc.).
|
|
2454
|
+
* Constant : UPDATE_BLOCK_INFO
|
|
2455
|
+
* @param data - Les données à envoyer.
|
|
2456
|
+
* @returns - Les données de réponse.
|
|
2457
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2458
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
2459
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2460
|
+
*/
|
|
2461
|
+
async updateInfo(data: Omit<UpdateBlockInfoData, "id" | "typeElement" | "block">): Promise<unknown> {
|
|
2462
|
+
|
|
2463
|
+
if (!this.id) {
|
|
2464
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// Réduit le type de l'entité aux seuls autorisés par UpdateBlockDescriptionData
|
|
2468
|
+
const t = this.getEntityType();
|
|
2469
|
+
if (t === "badges" || t === "news") {
|
|
2470
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_INFO.`, 400);
|
|
2471
|
+
}
|
|
2472
|
+
const typeElement = t as UpdateBlockInfoData["typeElement"];
|
|
2473
|
+
|
|
2474
|
+
const payload: UpdateBlockInfoData = {
|
|
2475
|
+
block: "info",
|
|
2476
|
+
id: this.id,
|
|
2477
|
+
typeElement,
|
|
2478
|
+
...data
|
|
2479
|
+
};
|
|
2480
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockInfo(payload));
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
/**
|
|
2484
|
+
* 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.
|
|
2485
|
+
* Constant : UPDATE_BLOCK_SOCIAL
|
|
2486
|
+
* @param data - Les données à envoyer.
|
|
2487
|
+
* @returns - Les données de réponse.
|
|
2488
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2489
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
2490
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2491
|
+
*/
|
|
2492
|
+
async updateSocial(data: Omit<UpdateBlockSocialData, "id" | "typeElement" | "block">): Promise<unknown> {
|
|
2493
|
+
if (!this.id) {
|
|
2494
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// Réduit le type de l'entité aux seuls autorisés par UpdateBlockDescriptionData
|
|
2498
|
+
const t = this.getEntityType();
|
|
2499
|
+
if (t === "badges" || t === "news" || t === "poi" || t === "events") {
|
|
2500
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_INFO.`, 400);
|
|
2501
|
+
}
|
|
2502
|
+
const typeElement = t as UpdateBlockSocialData["typeElement"];
|
|
2503
|
+
|
|
2504
|
+
const payload: UpdateBlockSocialData = {
|
|
2505
|
+
block: "network",
|
|
2506
|
+
id: this.id,
|
|
2507
|
+
typeElement,
|
|
2508
|
+
...data
|
|
2509
|
+
};
|
|
2510
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockSocial(payload));
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* @private
|
|
2515
|
+
*/
|
|
2516
|
+
private _isPostalAddress(a: any): a is PostalAddress {
|
|
2517
|
+
return !!a
|
|
2518
|
+
&& typeof a === "object"
|
|
2519
|
+
&& a["@type"] === "PostalAddress"
|
|
2520
|
+
&& typeof a.addressCountry === "string"
|
|
2521
|
+
&& typeof a.addressLocality === "string"
|
|
2522
|
+
&& typeof a.level1 === "string"
|
|
2523
|
+
&& typeof a.level1Name === "string"
|
|
2524
|
+
&& typeof a.codeInsee === "string"
|
|
2525
|
+
&& typeof a.localityId === "string";
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
/**
|
|
2529
|
+
* @private
|
|
2530
|
+
*/
|
|
2531
|
+
private _isGeo(a: any): a is Geo {
|
|
2532
|
+
return !!a
|
|
2533
|
+
&& typeof a === "object"
|
|
2534
|
+
&& (typeof a.latitude === "number" || typeof a.latitude === "string")
|
|
2535
|
+
&& (typeof a.longitude === "number" || typeof a.longitude === "string");
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* @private
|
|
2540
|
+
*/
|
|
2541
|
+
private _isGeoPosition(a: any): a is GeoPosition {
|
|
2542
|
+
return !!a
|
|
2543
|
+
&& typeof a === "object"
|
|
2544
|
+
&& a.type === "Point"
|
|
2545
|
+
&& Array.isArray(a.coordinates)
|
|
2546
|
+
&& a.coordinates.length === 2
|
|
2547
|
+
&& (typeof a.coordinates[0] === "number" || typeof a.coordinates[0] === "string")
|
|
2548
|
+
&& (typeof a.coordinates[1] === "number" || typeof a.coordinates[1] === "string")
|
|
2549
|
+
&& a.float === true;
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
/**
|
|
2553
|
+
* Mettre à jour les localités d'un élément : Permet de mettre à jour l'adresse et les informations géographiques d'un élément.
|
|
2554
|
+
* Constant : UPDATE_BLOCK_LOCALITY
|
|
2555
|
+
* @param data - Les données à envoyer.
|
|
2556
|
+
* @returns - Les données de réponse.
|
|
2557
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2558
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
2559
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2560
|
+
*/
|
|
2561
|
+
async updateLocality(data: Omit<UpdateBlockLocalityData, "id" | "typeElement" | "block">): Promise<unknown> {
|
|
2562
|
+
if (!this.id) {
|
|
2563
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// Réduit le type de l'entité aux seuls autorisés par UpdateBlockLocalityData
|
|
2567
|
+
const t = this.getEntityType();
|
|
2568
|
+
if (t === "badges" || t === "news" || t === "comments") {
|
|
2569
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_LOCALITY.`, 400);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Garde runtime pour s'assurer que 'address' est bien fourni
|
|
2573
|
+
if (!("address" in data)) {
|
|
2574
|
+
throw new ApiValidationError("Le champ 'address' est requis.", 400, ["address requis"]);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const addrUnknown = data.address as unknown;
|
|
2578
|
+
|
|
2579
|
+
if (!(addrUnknown === "" || this._isPostalAddress(addrUnknown))) {
|
|
2580
|
+
throw new ApiValidationError("Format de 'address' invalide.", 400, ["address invalide"]);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
const addr: UpdateBlockLocalityData["address"] = addrUnknown;
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
const typeElement: UpdateBlockLocalityData["typeElement"] = t;
|
|
2587
|
+
|
|
2588
|
+
const payload: UpdateBlockLocalityData = {
|
|
2589
|
+
block: "localities" as const,
|
|
2590
|
+
id: this.id,
|
|
2591
|
+
typeElement,
|
|
2592
|
+
address: addr,
|
|
2593
|
+
// On ne recopie que les champs autorisés pour éviter de casser le typage
|
|
2594
|
+
...( "geo" in data && this._isGeo(data.geo) ? { geo: data.geo } : {} ),
|
|
2595
|
+
...( "geoPosition" in data && this._isGeoPosition(data.geoPosition) ? { geoPosition: data.geoPosition } : {} ),
|
|
2596
|
+
...( "locality" in data ? { locality: data.locality } : {} ),
|
|
2597
|
+
};
|
|
2598
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockLocality(payload));
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* Mettre à jour le slug d'un élément : Permet de mettre à jour le slug pour une URL simplifiée.
|
|
2603
|
+
* Constant : UPDATE_BLOCK_SLUG
|
|
2604
|
+
* @param data - Les données à envoyer.
|
|
2605
|
+
* @param data.slug - Le nouveau slug à appliquer.
|
|
2606
|
+
* @returns - Les données de réponse.
|
|
2607
|
+
*/
|
|
2608
|
+
async updateSlug({ slug }: { slug: string }): Promise<unknown> {
|
|
2609
|
+
|
|
2610
|
+
if (!this.id) {
|
|
2611
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
const t = this.getEntityType();
|
|
2615
|
+
if (t === "badges" || t === "news" || t === "poi" || t === "comments") {
|
|
2616
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_LOCALITY.`, 400);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
const type = t as CheckData["type"];
|
|
2620
|
+
|
|
2621
|
+
try {
|
|
2622
|
+
await this.endpointApi.check({ block: "info", type, id: this.id, slug });
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
if(error instanceof ApiResponseError) {
|
|
2625
|
+
throw new ApiResponseError("Erreur lors de la vérification du slug.", error.status, error.responseData);
|
|
2626
|
+
}
|
|
2627
|
+
throw error;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const payload: UpdateBlockSlugData = { block: "info", typeElement: type, id: this.id, slug };
|
|
2631
|
+
|
|
2632
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockSlug(payload));
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
/**
|
|
2636
|
+
* Mettre à jour l'image de profil : Permet de mettre à jour l'image de profil d'un utilisateur ou d'une entité.
|
|
2637
|
+
* Constant : PROFIL_IMAGE
|
|
2638
|
+
* @param data - Les données à envoyer.
|
|
2639
|
+
* @param data.profil_avatar - L'image de profil à mettre à jour.
|
|
2640
|
+
* @returns - Les données de réponse.
|
|
2641
|
+
*/
|
|
2642
|
+
async updateImageProfil({ profil_avatar: image }: { profil_avatar: UploadInput }): Promise<unknown> {
|
|
2643
|
+
|
|
2644
|
+
if (!this.id) {
|
|
2645
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
const t = this.getEntityType();
|
|
2649
|
+
if (t === "badges" || t === "news" || t === "comments") {
|
|
2650
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_LOCALITY.`, 400);
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
const folder: NonNullable<ProfilImageData["pathParams"]>["folder"] = t;
|
|
2654
|
+
|
|
2655
|
+
image = await this._validateImage(image);
|
|
2656
|
+
|
|
2657
|
+
const data: ProfilImageData = { pathParams: { folder, ownerId: this.id }, profil_avatar: image as unknown as Record<string, unknown>, };
|
|
2658
|
+
return this.callIsConnected(() => this.endpointApi.profilImage(data));
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2661
|
+
/**
|
|
2662
|
+
* Crée une instance d'organisation et récupère son profil si nécessaire.
|
|
2663
|
+
*
|
|
2664
|
+
* @param organizationData - Les données pour initialiser l'organisation.
|
|
2665
|
+
* - Si { id } ou { slug } : récupère l'organisation existante (GET)
|
|
2666
|
+
* - Sinon : crée une nouvelle instance (CREATE)
|
|
2667
|
+
* @returns Une promesse qui résout l'objet Organisation créé.
|
|
2668
|
+
* @throws {Error} Si une erreur se produit lors de la création de l'organisation.
|
|
2669
|
+
*/
|
|
2670
|
+
async organization(organizationData: OrganizationInput = {}): Promise<Organization> {
|
|
2671
|
+
if(!this.isMe){
|
|
2672
|
+
throw new ApiError("Vous devez être connecté et être l'utilisateur pour créer une organisation.", 403);
|
|
2673
|
+
}
|
|
2674
|
+
const entity = await this.entity("organizations", organizationData);
|
|
2675
|
+
return entity as Organization;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/**
|
|
2679
|
+
* Crée une instance de projet et récupère son profil si nécessaire.
|
|
2680
|
+
*
|
|
2681
|
+
* @param projectData - Les données pour initialiser le projet.
|
|
2682
|
+
* - Si { id } ou { slug } : récupère le projet existant (GET)
|
|
2683
|
+
* - Sinon : crée une nouvelle instance (CREATE)
|
|
2684
|
+
* @returns Une promesse qui résout l'objet Projet créé.
|
|
2685
|
+
* @throws {Error} Si une erreur se produit lors de la création du projet.
|
|
2686
|
+
*/
|
|
2687
|
+
async project(projectData: ProjectInput = {}): Promise<Project> {
|
|
2688
|
+
// TODO: Vérifier si l'utilisateur est admin de l'organisation
|
|
2689
|
+
if(!this.isConnected){
|
|
2690
|
+
throw new ApiError("Vous devez être connecté pour créer un projet.", 401);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
const entity = await this.entity("projects", projectData);
|
|
2694
|
+
return entity as Project;
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
/**
|
|
2698
|
+
* Crée une instance de POI et la récupère si nécessaire.
|
|
2699
|
+
*
|
|
2700
|
+
* @param poiData - Les données pour initialiser le POI.
|
|
2701
|
+
* - Si { id } ou { slug } : récupère le POI existant (GET)
|
|
2702
|
+
* - Sinon : crée une nouvelle instance (CREATE)
|
|
2703
|
+
* @returns Une promesse qui résout l'objet POI créé.
|
|
2704
|
+
* @throws {Error} Si une erreur se produit lors de la création du POI.
|
|
2705
|
+
*/
|
|
2706
|
+
async poi(poiData: PoiInput = {}): Promise<Poi> {
|
|
2707
|
+
// TODO: Vérifier si l'utilisateur est admin de l'organisation
|
|
2708
|
+
if(!this.isConnected){
|
|
2709
|
+
throw new ApiError("Vous devez être connecté pour créer un POI.", 401);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
const entity = await this.entity("poi", poiData);
|
|
2713
|
+
return entity as Poi;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
/**
|
|
2717
|
+
* Crée une instance d'événement et la récupère si nécessaire.
|
|
2718
|
+
*
|
|
2719
|
+
* @param eventData - Les données pour initialiser l'événement.
|
|
2720
|
+
* - Si { id } ou { slug } : récupère l'événement existant (GET)
|
|
2721
|
+
* - Sinon : crée une nouvelle instance (CREATE)
|
|
2722
|
+
* @returns Une promesse qui résout l'objet Événement créé.
|
|
2723
|
+
* @throws {Error} Si une erreur se produit lors de la création de l'événement.
|
|
2724
|
+
*/
|
|
2725
|
+
async event(eventData: EventInput = {}): Promise<EventEntity> {
|
|
2726
|
+
// TODO: Vérifier si l'utilisateur est admin de l'organisation
|
|
2727
|
+
if(!this.isConnected){
|
|
2728
|
+
throw new ApiError("Vous devez être connecté pour créer un événement.", 401);
|
|
2729
|
+
}
|
|
2730
|
+
const entity = await this.entity("events", eventData);
|
|
2731
|
+
return entity as EventEntity;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
/**
|
|
2735
|
+
* Crée une instance de badge et la récupère si nécessaire.
|
|
2736
|
+
*
|
|
2737
|
+
* @param badgeData - Les données pour initialiser le badge.
|
|
2738
|
+
* - Si { id } : récupère le badge existant (GET)
|
|
2739
|
+
* - Sinon : crée une nouvelle instance (CREATE)
|
|
2740
|
+
* @returns Une promesse qui résout l'objet Badge créé.
|
|
2741
|
+
* @throws {Error} Si une erreur se produit lors de la création du badge.
|
|
2742
|
+
*/
|
|
2743
|
+
async badge(badgeData: BadgeInput = {}): Promise<Badge> {
|
|
2744
|
+
// TODO: Vérifier si l'utilisateur est admin de l'organisation
|
|
2745
|
+
if(!this.isConnected){
|
|
2746
|
+
throw new ApiError("Vous devez être connecté pour créer un badge.", 401);
|
|
2747
|
+
}
|
|
2748
|
+
const entity = await this.entity("badges", badgeData);
|
|
2749
|
+
return entity as Badge;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
/**
|
|
2753
|
+
* Crée une instance de news et la récupère si nécessaire.
|
|
2754
|
+
*
|
|
2755
|
+
* @param newsData - Les données pour initialiser la news.
|
|
2756
|
+
* - Si { id } : récupère la news existante (GET)
|
|
2757
|
+
* - Sinon : crée une nouvelle instance (CREATE)
|
|
2758
|
+
* @returns Une promesse qui résout l'objet News créé.
|
|
2759
|
+
* @throws {Error} Si une erreur se produit lors de la création de la news.
|
|
2760
|
+
*/
|
|
2761
|
+
async news(newsData: NewsInput = {}): Promise<News> {
|
|
2762
|
+
if(!this.isConnected){
|
|
2763
|
+
throw new ApiError("Vous devez être connecté.", 401);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
const entity = await this.entity("news", newsData);
|
|
2767
|
+
return entity as News;
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
/**
|
|
2771
|
+
* Récupérer les organisations d'une entitée : la liste des organisations dont l'entité est membre ou admin valide.
|
|
2772
|
+
* Constant : GET_ORGANIZATIONS_ADMIN | GET_ORGANIZATIONS_NO_ADMIN
|
|
2773
|
+
* @param data - Paramètres (partiels) de recherche/pagination.
|
|
2774
|
+
* @returns - Les données de réponse.
|
|
2775
|
+
*/
|
|
2776
|
+
async getOrganizations(data: Partial<GetOrganizationsAdminData | GetOrganizationsNoAdminData> = {}): Promise<PaginatorPage<Organization>> {
|
|
2777
|
+
data.searchType = this._getDefaultFromEndpoint("GET_ORGANIZATIONS_NO_ADMIN", "searchType") as GetOrganizationsNoAdminData["searchType"];
|
|
2778
|
+
// data.searchBy = "ALL";
|
|
2779
|
+
|
|
2780
|
+
const paginator = this._createPaginatorEngine({
|
|
2781
|
+
initialData: data,
|
|
2782
|
+
finalizer: async (finalData) => {
|
|
2783
|
+
if(this.isMe){
|
|
2784
|
+
finalData.pathParams = { type: this.getEntityType(), id: this.id };
|
|
2785
|
+
// NOTE : dans le schema je crois que si pas de finalData.filters alors le default ce fait avec finalData.pathParams
|
|
2786
|
+
// finalData.filters = {
|
|
2787
|
+
// [`links.members.${this.id}`]: { "$exists": true },
|
|
2788
|
+
// [`links.members.${this.id}.toBeValidated`]: { "$exists": false },
|
|
2789
|
+
// [`links.members.${this.id}.isInviting`]: { "$exists": false }
|
|
2790
|
+
// };
|
|
2791
|
+
} else {
|
|
2792
|
+
delete finalData?.pathParams;
|
|
2793
|
+
finalData.filters = {
|
|
2794
|
+
[`links.members.${this.id}`]: { "$exists": true },
|
|
2795
|
+
[`links.members.${this.id}.toBeValidated`]: { "$exists": false },
|
|
2796
|
+
[`links.members.${this.id}.isInviting`]: { "$exists": false }
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
const fetchFn = this.isMe
|
|
2801
|
+
? () => this.callIsMe(() => this.endpointApi.getOrganizationsAdmin(finalData))
|
|
2802
|
+
: () => this.endpointApi.getOrganizationsNoAdmin(finalData);
|
|
2803
|
+
|
|
2804
|
+
return fetchFn();
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
return paginator.next() as Promise<PaginatorPage<Organization>>;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
/**
|
|
2812
|
+
* Récupérer les projets d'une entitée : liste des projets de l'entité ou elle est "parent" ou "contributeur".
|
|
2813
|
+
* Constant : GET_PROJECTS_ADMIN | GET_PROJECTS_NO_ADMIN
|
|
2814
|
+
* @param data - Paramètres (partiels) de recherche/pagination.
|
|
2815
|
+
* @returns - Les données de réponse.
|
|
2816
|
+
*/
|
|
2817
|
+
async getProjects(data: Partial<GetProjectsAdminData | GetProjectsNoAdminData> = {}): Promise<PaginatorPage<Project>> {
|
|
2818
|
+
data.searchType = this._getDefaultFromEndpoint("GET_PROJECTS_ADMIN", "searchType") as GetProjectsAdminData["searchType"];
|
|
2819
|
+
// data.searchBy = "ALL";
|
|
2820
|
+
|
|
2821
|
+
const paginator = this._createPaginatorEngine({
|
|
2822
|
+
initialData: data,
|
|
2823
|
+
finalizer: async (finalData) => {
|
|
2824
|
+
if(this.isMe){
|
|
2825
|
+
finalData.pathParams = { type: this.getEntityType(), id: this.id };
|
|
2826
|
+
// NOTE : dans le schema je crois que si pas de finalData.filters alors le default ce fait avec finalData.pathParams
|
|
2827
|
+
// finalData.filters = {
|
|
2828
|
+
// "$or": {
|
|
2829
|
+
// [`links.contributors.${this.id}`]: { "$exists": true },
|
|
2830
|
+
// [`parent.${this.id}`]: { "$exists": true }
|
|
2831
|
+
// },
|
|
2832
|
+
// [`links.contributors.${this.id}`]: { "$exists": true }
|
|
2833
|
+
// };
|
|
2834
|
+
} else {
|
|
2835
|
+
delete finalData?.pathParams;
|
|
2836
|
+
finalData.filters = {
|
|
2837
|
+
"$or": {
|
|
2838
|
+
[`links.contributors.${this.id}`]: { "$exists": true },
|
|
2839
|
+
[`parent.${this.id}`]: { "$exists": true }
|
|
2840
|
+
},
|
|
2841
|
+
[`links.contributors.${this.id}`]: { "$exists": true }
|
|
2842
|
+
};
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
const fetchFn = this.isMe
|
|
2846
|
+
? () => this.callIsMe(() => this.endpointApi.getProjectsAdmin(finalData))
|
|
2847
|
+
: () => this.endpointApi.getProjectsNoAdmin(finalData);
|
|
2848
|
+
|
|
2849
|
+
return fetchFn();
|
|
2850
|
+
}
|
|
2851
|
+
});
|
|
2852
|
+
|
|
2853
|
+
return paginator.next() as Promise<PaginatorPage<Project>>;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
/**
|
|
2857
|
+
* Récupérer les POIs d'une entité : liste des POIs de l'entité ou elle est "parent".
|
|
2858
|
+
* Constant : GET_POIS_NO_ADMIN / GET_POIS_ADMIN
|
|
2859
|
+
* @param data - Paramètres (partiels) de recherche/pagination.
|
|
2860
|
+
* @returns - Les données de réponse.
|
|
2861
|
+
*/
|
|
2862
|
+
async getPois(data: Partial<GetPoisAdminData | GetPoisNoAdminData> = {}): Promise<PaginatorPage<Poi>> {
|
|
2863
|
+
data.searchType = this._getDefaultFromEndpoint("GET_POIS_ADMIN", "searchType") as GetPoisAdminData["searchType"];
|
|
2864
|
+
// data.searchBy = "ALL";
|
|
2865
|
+
|
|
2866
|
+
const paginator = this._createPaginatorEngine({
|
|
2867
|
+
initialData: data,
|
|
2868
|
+
finalizer: async (finalData) => {
|
|
2869
|
+
if(this.isMe){
|
|
2870
|
+
finalData.pathParams = { type: this.getEntityType(), id: this.id };
|
|
2871
|
+
// NOTE : dans le schema je crois que si pas de finalData.filters alors le default ce fait avec finalData.pathParams
|
|
2872
|
+
// finalData.filters = {
|
|
2873
|
+
// [`parent.${this.id}`]: { "$exists": true },
|
|
2874
|
+
// };
|
|
2875
|
+
} else {
|
|
2876
|
+
delete finalData?.pathParams;
|
|
2877
|
+
finalData.filters = {
|
|
2878
|
+
[`parent.${this.id}`]: { "$exists": true },
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
const fetchFn = this.isMe
|
|
2883
|
+
? () => this.callIsMe(() => this.endpointApi.getPoisAdmin(finalData))
|
|
2884
|
+
: () => this.endpointApi.getPoisNoAdmin(finalData);
|
|
2885
|
+
|
|
2886
|
+
return fetchFn();
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
|
|
2890
|
+
return paginator.next() as Promise<PaginatorPage<Poi>>;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
/**
|
|
2894
|
+
* Récupérer les abonnés d'une entité
|
|
2895
|
+
* Constant : GET_SUBSCRIBERS
|
|
2896
|
+
* @param data - Paramètres (partiels) de recherche/pagination.
|
|
2897
|
+
* @returns - Les données de réponse.
|
|
2898
|
+
*/
|
|
2899
|
+
async getSubscribers(data: Partial<GetSubscribersData> = {}): Promise<PaginatorPage<User>> {
|
|
2900
|
+
data.searchType = this._getDefaultFromEndpoint("GET_SUBSCRIBERS", "searchType") as GetSubscribersData["searchType"];
|
|
2901
|
+
// data.searchBy = "ALL";
|
|
2902
|
+
|
|
2903
|
+
const paginator = this._createPaginatorEngine({
|
|
2904
|
+
initialData: data,
|
|
2905
|
+
finalizer: async (finalData) => {
|
|
2906
|
+
delete finalData?.pathParams;
|
|
2907
|
+
|
|
2908
|
+
finalData.filters = {
|
|
2909
|
+
[`links.follows.${this.id}`]: { "$exists": true },
|
|
2910
|
+
[`links.follows.${this.id}.toBeValidated`]: { "$exists": false },
|
|
2911
|
+
[`links.follows.${this.id}.isInviting`]: { "$exists": false }
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2914
|
+
return this.endpointApi.getSubscribers(finalData);
|
|
2915
|
+
}
|
|
2916
|
+
});
|
|
2917
|
+
|
|
2918
|
+
return paginator.next() as Promise<PaginatorPage<User>>;
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
/**
|
|
2922
|
+
* Liste des badges créés par l'entité
|
|
2923
|
+
* Constant : GET_BADGES
|
|
2924
|
+
* @param data - Paramètres (partiels) de recherche/pagination.
|
|
2925
|
+
* @returns - Les données de réponse.
|
|
2926
|
+
*/
|
|
2927
|
+
async getBadgesIssuer(data: Partial<GetBadgesData> = {}): Promise<PaginatorPage<Badge>> {
|
|
2928
|
+
data.searchType = this._getDefaultFromEndpoint("GET_BADGES", "searchType") as GetBadgesData["searchType"];
|
|
2929
|
+
// data.searchBy = "ALL";
|
|
2930
|
+
|
|
2931
|
+
const paginator = this._createPaginatorEngine({
|
|
2932
|
+
initialData: data,
|
|
2933
|
+
finalizer: async (finalData) => {
|
|
2934
|
+
delete finalData?.pathParams;
|
|
2935
|
+
|
|
2936
|
+
finalData.filters = finalData.filters || { "preferences.private": false };
|
|
2937
|
+
finalData.filters["$or"] = {};
|
|
2938
|
+
finalData.filters["$or"][`issuer.${this.id}`] = { "$exists": true };
|
|
2939
|
+
|
|
2940
|
+
return this.endpointApi.getBadges(finalData);
|
|
2941
|
+
}
|
|
2942
|
+
});
|
|
2943
|
+
|
|
2944
|
+
return paginator.next() as Promise<PaginatorPage<Badge>>;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Récupérer les actualités : Récupère la liste d'actualités selon plusieurs critères.
|
|
2949
|
+
* Constant : GET_NEWS
|
|
2950
|
+
* @param data - Paramètres (partiels) de recherche/pagination.
|
|
2951
|
+
* @returns - Les données de réponse.
|
|
2952
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2953
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2954
|
+
*/
|
|
2955
|
+
async getNews(data: Partial<Omit<GetNewsData, "pathParams">> = {}): Promise<News[]> {
|
|
2956
|
+
|
|
2957
|
+
if (!this.id) {
|
|
2958
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
2959
|
+
}
|
|
2960
|
+
|
|
2961
|
+
const t = this.getEntityType();
|
|
2962
|
+
if (t === "badges" || t === "news" || t === "poi" || t === "events" || t === "comments") {
|
|
2963
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par GET_NEWS.`, 400);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
const type: NonNullable<GetNewsData["pathParams"]>["type"] = t;
|
|
2967
|
+
|
|
2968
|
+
const dateLimit = "dateLimit" in data && typeof data.dateLimit === "number" ? { dateLimit: data.dateLimit } : { dateLimit: 0 };
|
|
2969
|
+
const indexStep = "indexStep" in data && typeof data.indexStep === "number" ? { indexStep: data.indexStep } : { indexStep: 12 };
|
|
2970
|
+
const search = "search" in data && typeof data.search === "object" && data.search !== null && "name" in data.search && typeof data.search.name === "string" ? { search: { name: data.search.name } } : {};
|
|
2971
|
+
|
|
2972
|
+
const payload: GetNewsData = {
|
|
2973
|
+
pathParams: { type, id: this.id, isLive: true },
|
|
2974
|
+
...dateLimit,
|
|
2975
|
+
...indexStep,
|
|
2976
|
+
...search
|
|
2977
|
+
};
|
|
2978
|
+
|
|
2979
|
+
const arrayObjet = await this.endpointApi.getNews(payload);
|
|
2980
|
+
if(!Array.isArray(arrayObjet)){
|
|
2981
|
+
throw new ApiResponseError("Erreur lors de la récupération des actualités.", 500, arrayObjet as object);
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
const rawList = this._linkEntities(arrayObjet);
|
|
2985
|
+
|
|
2986
|
+
return this._createFilteredProxy(rawList);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
/**
|
|
2990
|
+
* Récupérer la galerie d'une entité.
|
|
2991
|
+
* Constant : GET_GALLERY
|
|
2992
|
+
* @param data - Paramètres (partiels) de recherche.
|
|
2993
|
+
* @returns - Les données de réponse.
|
|
2994
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
2995
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
2996
|
+
*/
|
|
2997
|
+
async getGallery(data: Partial<Omit<GetGalleryData, "pathParams">> & { pathParams?: { docType?: "image" | "file" } } = {}): Promise<Record<string, any>> {
|
|
2998
|
+
|
|
2999
|
+
if (!this.id) {
|
|
3000
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
const t = this.getEntityType();
|
|
3004
|
+
if (t === "badges" || t === "news" || t === "poi" || t === "events" || t === "comments") {
|
|
3005
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_LOCALITY.`, 400);
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
const type: NonNullable<GetGalleryData["pathParams"]>["type"] = t;
|
|
3009
|
+
|
|
3010
|
+
const payload: GetGalleryData = {
|
|
3011
|
+
pathParams: { type, id: this.id, docType: data.pathParams?.docType || "image" }
|
|
3012
|
+
};
|
|
3013
|
+
|
|
3014
|
+
const arrayObjet = await this.endpointApi.getGallery(payload);
|
|
3015
|
+
|
|
3016
|
+
return arrayObjet as Record<string, any>;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
/**
|
|
3020
|
+
* Soumet une demande pour rejoindre l'entité courante (ex. organisation, projet, événement...).
|
|
3021
|
+
* Si une invitation est en attente, elle est automatiquement acceptée.
|
|
3022
|
+
*
|
|
3023
|
+
* @returns - Résultat de la demande ou de la validation.
|
|
3024
|
+
* @throws {ApiError} - Si l'entité ne supporte pas l'action ou si une demande est déjà en cours.
|
|
3025
|
+
*/
|
|
3026
|
+
async requestToJoin(): Promise<unknown> {
|
|
3027
|
+
if (!this.isMe) {
|
|
3028
|
+
throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.", 401);
|
|
3029
|
+
}
|
|
3030
|
+
this._checkLinkableEntity();
|
|
3031
|
+
return this._submitLinkRequest();
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/**
|
|
3035
|
+
* Soumet une demande pour devenir administrateur de l'entité courante.
|
|
3036
|
+
* Si une invitation est en attente, elle est automatiquement validée.
|
|
3037
|
+
*
|
|
3038
|
+
* @returns - Résultat de la demande ou de la validation.
|
|
3039
|
+
* @throws {ApiError} - Si l'entité ne supporte pas l'action ou si une demande est déjà en cours.
|
|
3040
|
+
*/
|
|
3041
|
+
async requestToJoinAdmin(): Promise<unknown> {
|
|
3042
|
+
if (!this.isMe) {
|
|
3043
|
+
throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.", 401);
|
|
3044
|
+
}
|
|
3045
|
+
this._checkLinkableEntity();
|
|
3046
|
+
return this._submitLinkRequestAdmin();
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
/**
|
|
3050
|
+
* Accepte une invitation à rejoindre l'entité courante.
|
|
3051
|
+
* Ne fonctionne que si un lien avec `isInviting` est détecté.
|
|
3052
|
+
*
|
|
3053
|
+
* @returns - Résultat de l'acceptation de l'invitation.
|
|
3054
|
+
* @throws {ApiError} - Si aucune invitation n'est en attente ou si l'entité ne supporte pas cette action.
|
|
3055
|
+
*/
|
|
3056
|
+
async acceptInvitation(): Promise<unknown> {
|
|
3057
|
+
if (!this.isMe) {
|
|
3058
|
+
throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.", 401);
|
|
3059
|
+
}
|
|
3060
|
+
this._checkLinkableEntity();
|
|
3061
|
+
return this._acceptLinkRequest();
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/**
|
|
3065
|
+
* Se désengage de l'entité courante : quitte un rôle (membre, contributeur, etc.).
|
|
3066
|
+
*
|
|
3067
|
+
* @returns - Résultat de la déconnexion.
|
|
3068
|
+
* @throws {ApiError} - Si l'entité ne supporte pas l'action ou si l'utilisateur n'est pas lié à cette entité.
|
|
3069
|
+
*/
|
|
3070
|
+
async leave(): Promise<unknown> {
|
|
3071
|
+
if (!this.isMe) {
|
|
3072
|
+
throw new ApiError("Vous devez être connecté pour envoyer une demande de participation.", 401);
|
|
3073
|
+
}
|
|
3074
|
+
this._checkLinkableEntity();
|
|
3075
|
+
return this._leaveLinkRequest();
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
|
|
3079
|
+
/**
|
|
3080
|
+
* S'abonne à l'entité courante.
|
|
3081
|
+
*
|
|
3082
|
+
* @returns - Résultat de l'abonnement.
|
|
3083
|
+
* @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
|
|
3084
|
+
*/
|
|
3085
|
+
async follow(): Promise<unknown> {
|
|
3086
|
+
if (!this.isMe) {
|
|
3087
|
+
throw new ApiError("Vous devez être connecté pour suivre cette entité.", 401);
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3090
|
+
if (!this.id) {
|
|
3091
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
if(!this.userId) {
|
|
3095
|
+
throw new ApiError("Utilisateur non connecté.", 401);
|
|
3096
|
+
}
|
|
3097
|
+
|
|
3098
|
+
const t = this.getEntityType();
|
|
3099
|
+
if (t === "badges" || t === "news" || t === "poi" || t === "comments") {
|
|
3100
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_LOCALITY.`, 400);
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
const parentType = t as FollowData["parentType"];
|
|
3104
|
+
|
|
3105
|
+
const userLink = this.userContext?.serverData?.links?.["follows"]?.[this.id] || null;
|
|
3106
|
+
|
|
3107
|
+
if (!userLink) {
|
|
3108
|
+
const data: FollowData = {
|
|
3109
|
+
childId: this.userId,
|
|
3110
|
+
childType: "citoyens",
|
|
3111
|
+
parentType,
|
|
3112
|
+
parentId: this.id
|
|
3113
|
+
};
|
|
3114
|
+
const retour = await this.callIsMe(() => this.endpointApi.follow(data));
|
|
3115
|
+
// TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
|
|
3116
|
+
await this.userContext?.refresh();
|
|
3117
|
+
return retour;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
throw new ApiError("Vous êtes déjà abonné à cette entité.", 409);
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
|
|
3124
|
+
/**
|
|
3125
|
+
* Se désabonne de l'entité courante.
|
|
3126
|
+
*
|
|
3127
|
+
* @returns - Résultat de la désinscription.
|
|
3128
|
+
* @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
|
|
3129
|
+
*/
|
|
3130
|
+
async unfollow(): Promise<unknown> {
|
|
3131
|
+
if (!this.isMe) {
|
|
3132
|
+
throw new ApiError("Vous devez être connecté pour vous désabonner.", 401);
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
if (!this.id) {
|
|
3136
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
if(!this.userId) {
|
|
3140
|
+
throw new ApiError("Utilisateur non connecté.", 401);
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
const userLink = this.userContext?.serverData?.links?.["follows"]?.[this.id] || null;
|
|
3144
|
+
|
|
3145
|
+
const t = this.getEntityType();
|
|
3146
|
+
if (t === "badges" || t === "news" || t === "poi") {
|
|
3147
|
+
throw new ApiError(`Le type d'entité "${t}" n'est pas supporté par UPDATE_BLOCK_LOCALITY.`, 400);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
const parentType = t as DisconnectData["parentType"];
|
|
3151
|
+
|
|
3152
|
+
if (userLink) {
|
|
3153
|
+
const data: DisconnectData = {
|
|
3154
|
+
childId: this.userId,
|
|
3155
|
+
childType: "citoyens",
|
|
3156
|
+
parentType,
|
|
3157
|
+
parentId: this.id,
|
|
3158
|
+
connectType: "followers"
|
|
3159
|
+
};
|
|
3160
|
+
const retour = await this.callIsMe(() => this.endpointApi.disconnect(data));
|
|
3161
|
+
// TODO : reflechir au moyen de remplir parent.serverData et this.serverData avec les data de retour pour eviter un refresh
|
|
3162
|
+
await this.userContext?.refresh();
|
|
3163
|
+
return retour;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
throw new ApiError("Vous n'êtes pas abonné à cette entité.", 404);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
|
|
3170
|
+
/**
|
|
3171
|
+
* Vérifie si l'utilisateur est connecté et a accès à l'entité.
|
|
3172
|
+
*
|
|
3173
|
+
* @param action - Action à effectuer.
|
|
3174
|
+
* @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si l'entité n'est pas enregistrée.
|
|
3175
|
+
* @private
|
|
3176
|
+
*/
|
|
3177
|
+
private _checkAccess(action: string = "effectuer cette action"): void {
|
|
3178
|
+
if (!this.isMe) {
|
|
3179
|
+
throw new ApiError(`Vous devez être connecté pour ${action}.`, 401);
|
|
3180
|
+
}
|
|
3181
|
+
if (!this.id) {
|
|
3182
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
/**
|
|
3187
|
+
* Vérifie si l'utilisateur a un lien valide avec l'entité.
|
|
3188
|
+
*
|
|
3189
|
+
* @param userLink
|
|
3190
|
+
* @returns - `true` si le lien est valide, `false` sinon.
|
|
3191
|
+
* @protected
|
|
3192
|
+
*/
|
|
3193
|
+
protected _validateUserLink(userLink: MinimalUserLink | null | undefined): boolean {
|
|
3194
|
+
if (!userLink) return false;
|
|
3195
|
+
const { toBeValidated, isInviting } = userLink;
|
|
3196
|
+
return !toBeValidated && !isInviting;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/**
|
|
3200
|
+
* Vérifie si l'entité est d'un type spécifique.
|
|
3201
|
+
*
|
|
3202
|
+
* @param types - Types d'entité attendus.
|
|
3203
|
+
* @throws {ApiError} - Si l'entité n'est pas du type attendu.
|
|
3204
|
+
* @protected
|
|
3205
|
+
*/
|
|
3206
|
+
protected _assertEntityType(...types: string[]): void {
|
|
3207
|
+
const expectedTypes = Array.isArray(types[0]) ? types[0] : types;
|
|
3208
|
+
if (!expectedTypes.includes(this.getEntityType())) {
|
|
3209
|
+
throw new ApiError(`L'entité doit être de type : ${expectedTypes.join(", ")}, reçu : ${this.getEntityType()}`, 400);
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
/**
|
|
3214
|
+
* Vérifie si l'entité est liée à un type de lien spécifique.
|
|
3215
|
+
*
|
|
3216
|
+
* @param linkType - Type de lien à vérifier.
|
|
3217
|
+
* @returns - `true` si l'entité est liée, `false` sinon.
|
|
3218
|
+
* @protected
|
|
3219
|
+
*/
|
|
3220
|
+
protected _isLinked(linkType: string): boolean {
|
|
3221
|
+
if(!this.id) {
|
|
3222
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3223
|
+
}
|
|
3224
|
+
const links = this.userContext?.serverData?.links;
|
|
3225
|
+
if (!links) return false;
|
|
3226
|
+
return !!(links as Record<string, Record<string, unknown>>)[linkType]?.[this.id];
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
/**
|
|
3230
|
+
* Vérifie si l'utilisateur est l'auteur de l'entité.
|
|
3231
|
+
*
|
|
3232
|
+
* @returns - `true` si l'utilisateur est l'auteur, `false` sinon.
|
|
3233
|
+
* @throws {ApiError} - Si l'utilisateur n'est pas connecté ou si les données du serveur ne sont pas disponibles.
|
|
3234
|
+
*/
|
|
3235
|
+
isAuthor(): boolean {
|
|
3236
|
+
this._checkAccess("vérifier l'auteur");
|
|
3237
|
+
if (!this.serverData) {
|
|
3238
|
+
throw new ApiError("Aucune donnée serveur disponible.", 404);
|
|
3239
|
+
}
|
|
3240
|
+
if (!this.userId) {
|
|
3241
|
+
throw new ApiError("Utilisateur non connecté.", 401);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
const sd: { creator?: string | null } = this.serverData;
|
|
3245
|
+
|
|
3246
|
+
// Si pas de creator (données anciennes), l'utilisateur n'est pas l'auteur
|
|
3247
|
+
if (!sd.creator) {
|
|
3248
|
+
return false;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
return Boolean(this.userId && typeof sd.creator === "string" && sd.creator === this.userId);
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
/**
|
|
3255
|
+
* Vérifie si l'utilisateur est administrateur de l'entité.
|
|
3256
|
+
*
|
|
3257
|
+
* @returns - `true` si l'utilisateur est administrateur, `false` sinon.
|
|
3258
|
+
* @throws {ApiError}
|
|
3259
|
+
*/
|
|
3260
|
+
isAdmin(): boolean {
|
|
3261
|
+
this._checkAccess("vérifier l'administrateur.");
|
|
3262
|
+
this._assertEntityType("organizations", "projects", "events");
|
|
3263
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
3264
|
+
return this._validateUserLink(userLink) && userLink?.isAdmin === true && !userLink?.isAdminPending;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/**
|
|
3268
|
+
* Vérifie si l'utilisateur est soit l'auteur, soit administrateur de l'entité.
|
|
3269
|
+
*
|
|
3270
|
+
* @returns - `true` si l'utilisateur est l'auteur ou administrateur, `false` sinon.
|
|
3271
|
+
* @throws {ApiError}
|
|
3272
|
+
*/
|
|
3273
|
+
isAuthorOrAdmin(): boolean {
|
|
3274
|
+
this._checkAccess("vérifier l'auteur ou l'administrateur.");
|
|
3275
|
+
return this.isAuthor() || this.isAdmin();
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
/**
|
|
3279
|
+
* Vérifie si l'utilisateur est membre de l'entité.
|
|
3280
|
+
*
|
|
3281
|
+
* @returns - `true` si l'utilisateur est membre, `false` sinon.
|
|
3282
|
+
* @throws {ApiError}
|
|
3283
|
+
*/
|
|
3284
|
+
isMember(): boolean {
|
|
3285
|
+
this._checkAccess("vérifier le membre.");
|
|
3286
|
+
this._assertEntityType("organizations");
|
|
3287
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
3288
|
+
return this._validateUserLink(userLink);
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
/**
|
|
3292
|
+
* Vérifie si l'utilisateur est contributeur de l'entité.
|
|
3293
|
+
*
|
|
3294
|
+
* @returns - `true` si l'utilisateur est contributeur, `false` sinon.
|
|
3295
|
+
* @throws {ApiError}
|
|
3296
|
+
*/
|
|
3297
|
+
isContributor(): boolean {
|
|
3298
|
+
this._checkAccess("vérifier le contributeur.");
|
|
3299
|
+
this._assertEntityType("projects");
|
|
3300
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
3301
|
+
return this._validateUserLink(userLink);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
/**
|
|
3305
|
+
* Vérifie si l'utilisateur est participant de l'entité.
|
|
3306
|
+
*
|
|
3307
|
+
* @returns - `true` si l'utilisateur est participant, `false` sinon.
|
|
3308
|
+
* @throws {ApiError}
|
|
3309
|
+
*/
|
|
3310
|
+
isAttendee(): boolean {
|
|
3311
|
+
this._checkAccess("vérifier si vous êtes un participant.");
|
|
3312
|
+
this._assertEntityType("events");
|
|
3313
|
+
const userLink = this._getLinkFromConnectedUser();
|
|
3314
|
+
return this._validateUserLink(userLink);
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/**
|
|
3318
|
+
* Vérifie si l'utilisateur suit l'entité.
|
|
3319
|
+
*
|
|
3320
|
+
* @returns - `true` si l'utilisateur suit l'entité, `false` sinon.
|
|
3321
|
+
* @throws {ApiError}
|
|
3322
|
+
*/
|
|
3323
|
+
isFollower(): boolean {
|
|
3324
|
+
this._checkAccess("vérifier si il vous suit.");
|
|
3325
|
+
this._assertEntityType("citoyens","organizations", "projects", "events", "poi");
|
|
3326
|
+
return this._isLinked("followers");
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/**
|
|
3330
|
+
* Vérifie si l'utilisateur est abonné à l'entité.
|
|
3331
|
+
*
|
|
3332
|
+
* @returns - `true` si l'utilisateur est abonné, `false` sinon.
|
|
3333
|
+
* @throws {ApiError}
|
|
3334
|
+
*/
|
|
3335
|
+
isFollowing(): boolean {
|
|
3336
|
+
this._checkAccess("vérifier si vous le suivez.");
|
|
3337
|
+
this._assertEntityType("citoyens","organizations", "projects", "events", "poi");
|
|
3338
|
+
return this._isLinked("follows");
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
/**
|
|
3342
|
+
* Récupère le JSON personnalisé de l'entité
|
|
3343
|
+
*
|
|
3344
|
+
* @returns - Le JSON personnalisé de l'entité.
|
|
3345
|
+
* @throws {ApiError} - Si le slug de l'entité n'est pas défini.
|
|
3346
|
+
*/
|
|
3347
|
+
async getCostumJson(): Promise<unknown> {
|
|
3348
|
+
if (!this.serverData) {
|
|
3349
|
+
throw new ApiError("Aucune donnée serveur disponible.", 404);
|
|
3350
|
+
}
|
|
3351
|
+
const sd: { slug?: string | null } = this.serverData;
|
|
3352
|
+
|
|
3353
|
+
if (!sd || typeof sd.slug !== "string" || sd.slug.trim() === "") {
|
|
3354
|
+
throw new ApiError("slug de l'entité non défini", 400);
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
const payload: GetCostumJsonData = {
|
|
3358
|
+
pathParams: { slug: sd.slug }
|
|
3359
|
+
};
|
|
3360
|
+
|
|
3361
|
+
return this.endpointApi.getCostumJson(payload);
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
/**
|
|
3365
|
+
* Génère des plages d'index pour la pagination.
|
|
3366
|
+
*
|
|
3367
|
+
* @param searchType - Types de recherche.
|
|
3368
|
+
* @param indexStep - Pas d'index.
|
|
3369
|
+
* @param previousRanges - Plages précédentes.
|
|
3370
|
+
* @returns - Plages d'index générées.
|
|
3371
|
+
* @private
|
|
3372
|
+
*/
|
|
3373
|
+
private _generateRanges(searchType: string[], indexStep: number, previousRanges: Record<string, { indexMax: number }> = {}): Record<string, { indexMin: number; indexMax: number }> {
|
|
3374
|
+
const ranges: Record<string, { indexMin: number; indexMax: number }> = {};
|
|
3375
|
+
for (const type of searchType) {
|
|
3376
|
+
const previous = previousRanges[type] || { indexMax: 0 };
|
|
3377
|
+
ranges[type] = {
|
|
3378
|
+
indexMin: previous.indexMax || 0,
|
|
3379
|
+
indexMax: previous.indexMax + indexStep
|
|
3380
|
+
};
|
|
3381
|
+
}
|
|
3382
|
+
return ranges;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
/**
|
|
3386
|
+
* Normalise le compte des résultats.
|
|
3387
|
+
*
|
|
3388
|
+
* @param count - Compte des résultats.
|
|
3389
|
+
* @returns - Compte normalisé.
|
|
3390
|
+
* @private
|
|
3391
|
+
*/
|
|
3392
|
+
private _normalizeCount(count: any = {}): { total: number } & Record<string, any> {
|
|
3393
|
+
// suppression des indésirables
|
|
3394
|
+
delete count.spam;
|
|
3395
|
+
|
|
3396
|
+
// calcul du total (somme des valeurs numériques)
|
|
3397
|
+
count.total = Object.values(count).reduce((acc, val) => (acc as number) + (typeof val === "number" ? val : 0), 0);
|
|
3398
|
+
|
|
3399
|
+
return count;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
/**
|
|
3403
|
+
* Récupère une valeur par défaut depuis un endpoint donné.
|
|
3404
|
+
*
|
|
3405
|
+
* @param constant - Le nom unique de l'endpoint (ex: "GET_ORGANIZATIONS_NO_ADMIN")
|
|
3406
|
+
* @param path - Le chemin vers la propriété (ex: "searchType")
|
|
3407
|
+
* @returns La valeur par défaut, ou undefined si non trouvée
|
|
3408
|
+
*/
|
|
3409
|
+
_getDefaultFromEndpoint(constant: string, path: string): unknown {
|
|
3410
|
+
const requestSchema = this.apiClient.getRequestSchema(constant);
|
|
3411
|
+
if (!requestSchema?.properties?.[path]) return undefined;
|
|
3412
|
+
return requestSchema.properties[path].default;
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
/**
|
|
3416
|
+
* Coeur de pagination stateless et réutilisable.
|
|
3417
|
+
*/
|
|
3418
|
+
_createPaginatorEngine<TData extends Record<string, any>, TOut>({ initialData, finalizer }: {
|
|
3419
|
+
initialData: Partial<TData>,
|
|
3420
|
+
finalizer: (data: TData) => Promise<FinalizerResult<TOut>>
|
|
3421
|
+
}): { next: () => Promise<PaginatorPage<TOut>> } {
|
|
3422
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
3423
|
+
const Entity = this;
|
|
3424
|
+
|
|
3425
|
+
const state: { cursor: PaginationCursor | null; count: number; index: number; history: PaginationCursor[]; sizes: number[] } = { cursor: null, count: 0, index: 0, history: [], sizes: [] };
|
|
3426
|
+
|
|
3427
|
+
const hasStep = (d: PaginationCursor | null | undefined) => Boolean(d?.indexStep && d.indexStep > 0);
|
|
3428
|
+
|
|
3429
|
+
async function getPage(isNext = false) {
|
|
3430
|
+
const data: PaginationCursor = { ...initialData };
|
|
3431
|
+
|
|
3432
|
+
if (!state.cursor || (!isNext && state.history.length === 0)) {
|
|
3433
|
+
state.count = 0;
|
|
3434
|
+
state.index = 0;
|
|
3435
|
+
state.history = [];
|
|
3436
|
+
state.sizes = [];
|
|
3437
|
+
|
|
3438
|
+
// hydrate data pour le premier appel
|
|
3439
|
+
data.countType = data.searchType;
|
|
3440
|
+
|
|
3441
|
+
if (!data.searchBy && hasStep(data)) {
|
|
3442
|
+
data.ranges = Entity._generateRanges(
|
|
3443
|
+
data.searchType as string[],
|
|
3444
|
+
data.indexStep as number
|
|
3445
|
+
);
|
|
3446
|
+
data.indexMin = data.indexMin ?? 0;
|
|
3447
|
+
data.indexMax = data.indexMax ?? data.indexStep;
|
|
3448
|
+
} else if (data.searchBy === "ALL" && hasStep(data)) {
|
|
3449
|
+
data.indexMin = data.indexMin ?? 0;
|
|
3450
|
+
data.indexMax = data.indexMax ?? data.indexStep;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
state.cursor = { ...data };
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
const cursor: PaginationCursor = state.cursor!;
|
|
3457
|
+
|
|
3458
|
+
if (isNext && cursor) {
|
|
3459
|
+
state.history.push({ ...cursor });
|
|
3460
|
+
|
|
3461
|
+
if (!cursor.searchBy && hasStep(cursor)) {
|
|
3462
|
+
cursor.ranges = Entity._generateRanges(
|
|
3463
|
+
cursor.searchType as string[],
|
|
3464
|
+
cursor.indexStep as number,
|
|
3465
|
+
cursor.ranges as Record<string, { indexMax: number }>
|
|
3466
|
+
);
|
|
3467
|
+
cursor.indexMin = cursor.indexMax ?? 0;
|
|
3468
|
+
cursor.indexMax = (cursor.indexMax ?? 0) + (cursor.indexStep as number);
|
|
3469
|
+
} else if (cursor.searchBy === "ALL" && hasStep(cursor)) {
|
|
3470
|
+
cursor.indexMin = cursor.indexMax ?? 0;
|
|
3471
|
+
cursor.indexMax = (cursor.indexMax ?? 0) + (cursor.indexStep as number);
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
state.cursor = { ...cursor };
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3477
|
+
const finalizerInput: PaginationCursor = { ...(state.cursor || {}) };
|
|
3478
|
+
|
|
3479
|
+
if (!isNext) {
|
|
3480
|
+
const st = finalizerInput.searchType;
|
|
3481
|
+
if (!Array.isArray(st) || st.length === 0) {
|
|
3482
|
+
throw new Error("searchType non défini");
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
const result = await finalizer(finalizerInput as TData);
|
|
3487
|
+
|
|
3488
|
+
if (!Array.isArray(result.results)) {
|
|
3489
|
+
throw new Error("Les résultats doivent être un tableau");
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
state.count += result.results.length;
|
|
3493
|
+
state.sizes.push(result.results.length);
|
|
3494
|
+
if (isNext) state.index++;
|
|
3495
|
+
|
|
3496
|
+
const count = Entity._normalizeCount(result.count);
|
|
3497
|
+
const rawList = Entity._linkEntities(result.results);
|
|
3498
|
+
|
|
3499
|
+
const hasNext = hasStep(finalizerInput) && state.count < count.total;
|
|
3500
|
+
const hasPrev = state.history.length > 0;
|
|
3501
|
+
|
|
3502
|
+
return {
|
|
3503
|
+
count,
|
|
3504
|
+
results: rawList,
|
|
3505
|
+
pageIndex: state.index,
|
|
3506
|
+
pageNumber: state.index + 1,
|
|
3507
|
+
hasNext,
|
|
3508
|
+
hasPrev,
|
|
3509
|
+
next: hasNext ? () => getPage(true) : undefined,
|
|
3510
|
+
prev: hasPrev
|
|
3511
|
+
? async () => {
|
|
3512
|
+
const previous: PaginationCursor = state.history.pop()!;
|
|
3513
|
+
const lastPageSize = state.sizes.pop() ?? 0;
|
|
3514
|
+
state.count -= lastPageSize;
|
|
3515
|
+
state.index = Math.max(0, state.index - 1);
|
|
3516
|
+
state.cursor = { ...previous };
|
|
3517
|
+
return getPage(false);
|
|
3518
|
+
}
|
|
3519
|
+
: undefined
|
|
3520
|
+
};
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
return { next: () => getPage(false) };
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
/**
|
|
3527
|
+
* Injection de contexte Communecter dans une requête finalizer.
|
|
3528
|
+
*/
|
|
3529
|
+
_withCostumContext<TIn extends Record<string, unknown>, TOut>(
|
|
3530
|
+
baseFinalizer: (data: TIn & {
|
|
3531
|
+
costumSlug: string,
|
|
3532
|
+
contextId: string,
|
|
3533
|
+
contextType: EntityType,
|
|
3534
|
+
sourceKey: string[]
|
|
3535
|
+
}) => Promise<TOut>
|
|
3536
|
+
): (data: TIn) => Promise<TOut> {
|
|
3537
|
+
return async (data: TIn) => {
|
|
3538
|
+
if (!this.serverData) {
|
|
3539
|
+
throw new ApiError("Aucune donnée serveur disponible.", 404);
|
|
3540
|
+
}
|
|
3541
|
+
const sd: { slug?: string | null, id?: string | null } = this.serverData;
|
|
3542
|
+
|
|
3543
|
+
if (!sd || typeof sd.slug !== "string" || sd.slug.trim() === "") {
|
|
3544
|
+
throw new ApiError("slug de l'entité non défini", 400);
|
|
3545
|
+
}
|
|
3546
|
+
if (!sd.id || typeof sd.id !== "string") {
|
|
3547
|
+
throw new ApiError("id de l'entité non défini", 400);
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// sourceKey peut être absent ou non-tableau : on normalise
|
|
3551
|
+
const inSourceKey: string[] = Array.isArray(data?.sourceKey)
|
|
3552
|
+
? (data.sourceKey as string[])
|
|
3553
|
+
: [];
|
|
3554
|
+
|
|
3555
|
+
const finalData = {
|
|
3556
|
+
...(data || {}),
|
|
3557
|
+
costumSlug: sd.slug,
|
|
3558
|
+
contextId: sd.id,
|
|
3559
|
+
contextType: this.getEntityType(),
|
|
3560
|
+
sourceKey: [...inSourceKey, sd.slug] as string[]
|
|
3561
|
+
} as TIn & {
|
|
3562
|
+
costumSlug: string;
|
|
3563
|
+
contextId: string;
|
|
3564
|
+
contextType: EntityType;
|
|
3565
|
+
sourceKey: string[];
|
|
3566
|
+
};
|
|
3567
|
+
|
|
3568
|
+
return baseFinalizer(finalData);
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
/**
|
|
3573
|
+
* ───────────────────────────────
|
|
3574
|
+
* custom
|
|
3575
|
+
* ───────────────────────────────
|
|
3576
|
+
*/
|
|
3577
|
+
|
|
3578
|
+
/**
|
|
3579
|
+
* Recherche globale liée au *costum* (stateless).
|
|
3580
|
+
* Injecte automatiquement le contexte (slug/id/type) via `_withCostumContext`,
|
|
3581
|
+
* puis pagine via `_createPaginatorEngine`. Cette méthode renvoie **directement
|
|
3582
|
+
* la première page** (rétro-compatible) avec `.results`, `.count`, et des helpers
|
|
3583
|
+
* de pagination si disponibles.
|
|
3584
|
+
*
|
|
3585
|
+
* Remarque : les éléments de `results` peuvent être hétérogènes (multi-collections)
|
|
3586
|
+
* et éventuellement « liés » en entités par le moteur interne de pagination.
|
|
3587
|
+
*
|
|
3588
|
+
* @param data
|
|
3589
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3590
|
+
* par le moteur de pagination et le contexte).
|
|
3591
|
+
* @returns
|
|
3592
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3593
|
+
* @throws {ApiResponseError}
|
|
3594
|
+
* @throws {Error}
|
|
3595
|
+
*
|
|
3596
|
+
* @example
|
|
3597
|
+
* // Recherche simple (première page)
|
|
3598
|
+
* const page = await entity.searchCostum({ name: "marseille", searchType: ["projects"], indexStep: 12, indexMin: 0 });
|
|
3599
|
+
* console.log(page.results, page.count.total);
|
|
3600
|
+
*
|
|
3601
|
+
* @example
|
|
3602
|
+
* // Paginer
|
|
3603
|
+
* if (page.hasNext) {
|
|
3604
|
+
* const nextPage = await page.next();
|
|
3605
|
+
* console.log(nextPage.pageNumber, nextPage.results.length);
|
|
3606
|
+
* }
|
|
3607
|
+
*/
|
|
3608
|
+
async searchCostum(data: Partial<GlobalAutocompleteCostumData> = {}): Promise<PaginatorPage<any>> {
|
|
3609
|
+
const paginator = this._createPaginatorEngine({
|
|
3610
|
+
initialData: data,
|
|
3611
|
+
finalizer: this._withCostumContext(
|
|
3612
|
+
(finalData: GlobalAutocompleteCostumData) => this.endpointApi.globalAutocompleteCostum(finalData)
|
|
3613
|
+
),
|
|
3614
|
+
});
|
|
3615
|
+
return paginator.next() as Promise<PaginatorPage<any>>;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
/**
|
|
3619
|
+
* @param data
|
|
3620
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3621
|
+
* par le moteur de pagination et le contexte).
|
|
3622
|
+
* @returns
|
|
3623
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3624
|
+
* @throws {ApiResponseError}
|
|
3625
|
+
* @throws {Error}
|
|
3626
|
+
*/
|
|
3627
|
+
async costumEventRequestActors(data: Partial<Omit<CostumEventRequestActorsData, "pathParams">> = {}): Promise<unknown> {
|
|
3628
|
+
|
|
3629
|
+
if(!this.id) {
|
|
3630
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
const fullData = {
|
|
3634
|
+
pathParams: {
|
|
3635
|
+
id: this.id,
|
|
3636
|
+
type: this.getEntityType()
|
|
3637
|
+
},
|
|
3638
|
+
...data
|
|
3639
|
+
};
|
|
3640
|
+
|
|
3641
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3642
|
+
(finalData: CostumEventRequestActorsData) => this.endpointApi.costumEventRequestActors(finalData)
|
|
3643
|
+
);
|
|
3644
|
+
|
|
3645
|
+
const actors = await wrappedFinalizer(fullData);
|
|
3646
|
+
|
|
3647
|
+
// const transformedActors = actors
|
|
3648
|
+
// .filter(actor => {
|
|
3649
|
+
// const isValid = !!actor?.type;
|
|
3650
|
+
// if (!isValid) {
|
|
3651
|
+
// this.apiClient._logger?.warn?.(`Objet ignoré car sans 'type' : ${actor?.id}`);
|
|
3652
|
+
// }
|
|
3653
|
+
// return isValid;
|
|
3654
|
+
// })
|
|
3655
|
+
// .map(({ id, name, type, ...meta }) => ({
|
|
3656
|
+
// id,
|
|
3657
|
+
// name,
|
|
3658
|
+
// collection: type,
|
|
3659
|
+
// meta
|
|
3660
|
+
// }));
|
|
3661
|
+
|
|
3662
|
+
// const rawList = this._linkEntities(transformedActors);
|
|
3663
|
+
|
|
3664
|
+
return actors;
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
/**
|
|
3668
|
+
* @param data
|
|
3669
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3670
|
+
* par le moteur de pagination et le contexte).
|
|
3671
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3672
|
+
* @throws {ApiResponseError}
|
|
3673
|
+
* @throws {Error}
|
|
3674
|
+
*/
|
|
3675
|
+
async costumEventRequestSubevents(data: Partial<Omit<CostumEventRequestSubeventsData, "pathParams">> = {}): Promise<unknown> {
|
|
3676
|
+
|
|
3677
|
+
if(!this.id) {
|
|
3678
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
const fullData = {
|
|
3682
|
+
pathParams: {
|
|
3683
|
+
id: this.id,
|
|
3684
|
+
type: this.getEntityType()
|
|
3685
|
+
},
|
|
3686
|
+
...data
|
|
3687
|
+
};
|
|
3688
|
+
|
|
3689
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3690
|
+
(finalData: CostumEventRequestSubeventsData) => this.endpointApi.costumEventRequestSubevents(finalData)
|
|
3691
|
+
);
|
|
3692
|
+
|
|
3693
|
+
return wrappedFinalizer(fullData);
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
/**
|
|
3697
|
+
* @param data
|
|
3698
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3699
|
+
* par le moteur de pagination et le contexte).
|
|
3700
|
+
* @returns
|
|
3701
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3702
|
+
* @throws {ApiResponseError}
|
|
3703
|
+
* @throws {Error}
|
|
3704
|
+
*/
|
|
3705
|
+
async costumEventRequestDates(data: Partial<Omit<CostumEventRequestDatesData, "pathParams">> = {}): Promise<unknown> {
|
|
3706
|
+
|
|
3707
|
+
if(!this.id) {
|
|
3708
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
const fullData = {
|
|
3712
|
+
pathParams: {
|
|
3713
|
+
id: this.id,
|
|
3714
|
+
type: this.getEntityType()
|
|
3715
|
+
},
|
|
3716
|
+
...data
|
|
3717
|
+
};
|
|
3718
|
+
|
|
3719
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3720
|
+
(finalData: CostumEventRequestDatesData) => this.endpointApi.costumEventRequestDates(finalData)
|
|
3721
|
+
);
|
|
3722
|
+
|
|
3723
|
+
return wrappedFinalizer(fullData);
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
/**
|
|
3727
|
+
* @param data
|
|
3728
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3729
|
+
* par le moteur de pagination et le contexte).
|
|
3730
|
+
* @returns
|
|
3731
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3732
|
+
* @throws {ApiResponseError}
|
|
3733
|
+
* @throws {Error}
|
|
3734
|
+
*/
|
|
3735
|
+
async costumEventRequestElementEvent(data: Partial<Omit<CostumEventRequestElementEventData, "pathParams">> = {}): Promise<unknown> {
|
|
3736
|
+
|
|
3737
|
+
if(!this.id) {
|
|
3738
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
const fullData = {
|
|
3742
|
+
pathParams: {
|
|
3743
|
+
id: this.id,
|
|
3744
|
+
type: this.getEntityType()
|
|
3745
|
+
},
|
|
3746
|
+
...data
|
|
3747
|
+
};
|
|
3748
|
+
|
|
3749
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3750
|
+
(finalData: CostumEventRequestElementEventData) => this.endpointApi.costumEventRequestElementEvent(finalData)
|
|
3751
|
+
);
|
|
3752
|
+
|
|
3753
|
+
return wrappedFinalizer(fullData);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
/**
|
|
3757
|
+
* @param data
|
|
3758
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3759
|
+
* par le moteur de pagination et le contexte).
|
|
3760
|
+
* @returns
|
|
3761
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3762
|
+
* @throws {ApiResponseError}
|
|
3763
|
+
* @throws {Error}
|
|
3764
|
+
*/
|
|
3765
|
+
async costumEventRequestCategories(data: Partial<Omit<CostumEventRequestCategoriesData, "pathParams">> = {}): Promise<unknown> {
|
|
3766
|
+
|
|
3767
|
+
if(!this.id) {
|
|
3768
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
const fullData = {
|
|
3772
|
+
pathParams: {
|
|
3773
|
+
id: this.id,
|
|
3774
|
+
type: this.getEntityType()
|
|
3775
|
+
},
|
|
3776
|
+
...data
|
|
3777
|
+
};
|
|
3778
|
+
|
|
3779
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3780
|
+
(finalData: CostumEventRequestCategoriesData) => this.endpointApi.costumEventRequestCategories(finalData)
|
|
3781
|
+
);
|
|
3782
|
+
|
|
3783
|
+
return wrappedFinalizer(fullData);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
/**
|
|
3787
|
+
* @param data
|
|
3788
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3789
|
+
* par le moteur de pagination et le contexte).
|
|
3790
|
+
* @returns
|
|
3791
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3792
|
+
* @throws {ApiResponseError}
|
|
3793
|
+
* @throws {Error}
|
|
3794
|
+
*/
|
|
3795
|
+
async costumEventRequestEvent(data: Partial<Omit<CostumEventRequestEventData, "pathParams">> = {}): Promise<unknown> {
|
|
3796
|
+
|
|
3797
|
+
if(!this.id) {
|
|
3798
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
const fullData = {
|
|
3802
|
+
pathParams: {
|
|
3803
|
+
id: this.id,
|
|
3804
|
+
type: this.getEntityType()
|
|
3805
|
+
},
|
|
3806
|
+
...data
|
|
3807
|
+
};
|
|
3808
|
+
|
|
3809
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3810
|
+
(finalData: CostumEventRequestEventData) => this.endpointApi.costumEventRequestEvent(finalData)
|
|
3811
|
+
);
|
|
3812
|
+
|
|
3813
|
+
return wrappedFinalizer(fullData);
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
/**
|
|
3817
|
+
* @param data
|
|
3818
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3819
|
+
* par le moteur de pagination et le contexte).
|
|
3820
|
+
* @returns
|
|
3821
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3822
|
+
* @throws {ApiResponseError}
|
|
3823
|
+
* @throws {Error}
|
|
3824
|
+
*/
|
|
3825
|
+
async costumEventRequestLinkTlToEvent(
|
|
3826
|
+
data: Pick<CostumEventRequestLinkTlToEventData, "tl" | "event"> &
|
|
3827
|
+
Partial<Omit<CostumEventRequestLinkTlToEventData, "tl" | "event" | "pathParams">>
|
|
3828
|
+
): Promise<unknown> {
|
|
3829
|
+
|
|
3830
|
+
if(!this.id) {
|
|
3831
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
if(!data.tl && !data.event) {
|
|
3835
|
+
throw new ApiError("Les paramètres 'tl' et 'event' sont requis.", 400);
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
if(typeof data.tl !== "string" || typeof data.event !== "string") {
|
|
3839
|
+
throw new ApiError("Les paramètres 'tl' et 'event' doivent être des chaînes de caractères.", 400);
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
const payload: CostumEventRequestLinkTlToEventData = {
|
|
3843
|
+
pathParams: {
|
|
3844
|
+
id: this.id,
|
|
3845
|
+
type: this.getEntityType()
|
|
3846
|
+
},
|
|
3847
|
+
...data
|
|
3848
|
+
};
|
|
3849
|
+
|
|
3850
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3851
|
+
(finalData: CostumEventRequestLinkTlToEventData) => this.endpointApi.costumEventRequestLinkTlToEvent(finalData)
|
|
3852
|
+
);
|
|
3853
|
+
|
|
3854
|
+
return wrappedFinalizer(payload);
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
/**
|
|
3858
|
+
* @param data
|
|
3859
|
+
* Paramètres de recherche (partiels — les valeurs manquantes sont complétées
|
|
3860
|
+
* par le moteur de pagination et le contexte).
|
|
3861
|
+
* @returns
|
|
3862
|
+
* Première page paginée : `results` (T[]), `count.total`, `hasNext`, etc.
|
|
3863
|
+
* @throws {ApiResponseError}
|
|
3864
|
+
* @throws {Error}
|
|
3865
|
+
*/
|
|
3866
|
+
async costumEventRequestLoadContextTag(data: Partial<Omit<CostumEventRequestLoadContextTagData, "pathParams">> = {}): Promise<unknown> {
|
|
3867
|
+
|
|
3868
|
+
if(!this.id) {
|
|
3869
|
+
throw new ApiError(`${this.constructor.name} non enregistrée.`, 404);
|
|
3870
|
+
}
|
|
3871
|
+
|
|
3872
|
+
const fullData = {
|
|
3873
|
+
pathParams: {
|
|
3874
|
+
id: this.id,
|
|
3875
|
+
type: this.getEntityType()
|
|
3876
|
+
},
|
|
3877
|
+
...data
|
|
3878
|
+
};
|
|
3879
|
+
|
|
3880
|
+
const wrappedFinalizer = this._withCostumContext(
|
|
3881
|
+
(finalData: CostumEventRequestLoadContextTagData) => this.endpointApi.costumEventRequestLoadContextTag(finalData)
|
|
3882
|
+
);
|
|
3883
|
+
|
|
3884
|
+
return wrappedFinalizer(fullData);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
export default BaseEntity;
|