@communecter/cocolight-api-client 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cocolight-api-client.browser.js +2 -2
- package/dist/cocolight-api-client.cjs +1 -1
- package/dist/cocolight-api-client.mjs.js +1 -1
- package/package.json +2 -1
- package/src/api/BaseEntity.js +1420 -34
- package/src/api/EndpointApi.js +443 -924
- package/src/api/EndpointApi.types.d.ts +3463 -0
- package/src/api/Organization.js +77 -0
- package/src/api/User.js +91 -327
- package/src/mixin/DraftStateMixin.js +0 -176
- package/src/mixin/EntityMixin.js +0 -428
- package/src/mixin/MutualEntityMixin.js +0 -269
- package/src/mixin/UtilMixin.js +0 -300
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { ApiResponseError } from "../error.js";
|
|
2
|
-
|
|
3
|
-
// MutualEntityMixin.js
|
|
4
|
-
export const MutualEntityMixin = {
|
|
5
|
-
/**
|
|
6
|
-
* Résout l'identifiant de l'entité si seul le slug est fourni.
|
|
7
|
-
* @param {string} type - Le type d'entité (ex : "citoyens", "organizations", "projects").
|
|
8
|
-
* @returns {Promise<string>} L'identifiant résolu.
|
|
9
|
-
*/
|
|
10
|
-
async resolveId(type) {
|
|
11
|
-
if (!this.id && this.slug) {
|
|
12
|
-
try {
|
|
13
|
-
const data = await this.endpointApi.getElementsKey({
|
|
14
|
-
pathParams:{
|
|
15
|
-
slug: this.slug
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
if(data?.contextId && data?.contextType === type) {
|
|
19
|
-
this._id(data.contextId);
|
|
20
|
-
} else {
|
|
21
|
-
throw new ApiResponseError(`Le slug ${this.slug} ne correspond pas à un ${type}`, 200, data);
|
|
22
|
-
}
|
|
23
|
-
} catch (error) {
|
|
24
|
-
if(error instanceof ApiResponseError) {
|
|
25
|
-
if(error?.responseData?.contextType !== type) {
|
|
26
|
-
throw error;
|
|
27
|
-
} else {
|
|
28
|
-
throw new ApiResponseError(`Impossible de récupérer l'identifiant pour le slug ${this.slug}`, error.status, error.responseData);
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
throw error;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return this.id;
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Récupère le profil public de l'entité.
|
|
40
|
-
* @returns {Promise<Object>} Les données du profil public.
|
|
41
|
-
*/
|
|
42
|
-
async getPublicProfile() {
|
|
43
|
-
await this.resolveId(this.getEntityType());
|
|
44
|
-
return this.endpointApi.getElementsAbout({ pathParams: { id: this.id, type: this.getEntityType() } });
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Récupère la classe d'entité et ses dépendances à partir du type d'entité.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} entityType - Le type d'entité.
|
|
51
|
-
* @return {{ entityClass: Function, deps: Object } | null}
|
|
52
|
-
*/
|
|
53
|
-
_getEntityMeta(entityType) {
|
|
54
|
-
const selfClass = this.constructor;
|
|
55
|
-
const selfTag = this.__entityTag;
|
|
56
|
-
|
|
57
|
-
const commonDeps = {
|
|
58
|
-
EndpointApi: this.deps.EndpointApi,
|
|
59
|
-
User: selfTag === "User" ? selfClass : this.deps.User,
|
|
60
|
-
Organization: selfTag === "Organization" ? selfClass : this.deps.Organization,
|
|
61
|
-
Project: selfTag === "Project" ? selfClass : this.deps.Project,
|
|
62
|
-
Event: selfTag === "Event" ? selfClass : this.deps.Event,
|
|
63
|
-
Poi: selfTag === "Poi" ? selfClass : this.deps.Poi,
|
|
64
|
-
Badge: selfTag === "Badge" ? selfClass : this.deps.Badge,
|
|
65
|
-
News: selfTag === "News" ? selfClass : this.deps.News
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const map = {
|
|
69
|
-
citoyens: { entityClass: commonDeps.User, deps: commonDeps },
|
|
70
|
-
organizations:{ entityClass: commonDeps.Organization, deps: commonDeps },
|
|
71
|
-
projects: { entityClass: commonDeps.Project, deps: commonDeps },
|
|
72
|
-
events: { entityClass: commonDeps.Event, deps: { ...commonDeps, Badge: undefined } },
|
|
73
|
-
poi: { entityClass: commonDeps.Poi, deps: { ...commonDeps, Badge: undefined, News: undefined } },
|
|
74
|
-
news: { entityClass: commonDeps.News, deps: { ...commonDeps } },
|
|
75
|
-
badges: { entityClass: commonDeps.Badge, deps: {
|
|
76
|
-
EndpointApi: commonDeps.EndpointApi,
|
|
77
|
-
User: commonDeps.User,
|
|
78
|
-
Organization: commonDeps.Organization,
|
|
79
|
-
Project: commonDeps.Project
|
|
80
|
-
} }
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
return map[entityType] || null;
|
|
84
|
-
},
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Lier des données d'entité à une instance d'entité.
|
|
88
|
-
*
|
|
89
|
-
* @param {string} entityType - Le type d'entité (ex : "citoyens", "organisations", "projets").
|
|
90
|
-
* @param {Object} entityData - Les données de l'entité à lier.
|
|
91
|
-
* @return {Object} L'entité liée.
|
|
92
|
-
*/
|
|
93
|
-
linkEntity(entityType, entityData) {
|
|
94
|
-
const meta = this._getEntityMeta(entityType);
|
|
95
|
-
if (!meta) return entityData;
|
|
96
|
-
return meta.entityClass.fromServerData(entityData, this, meta.deps);
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Récupère et lie une entité à partir de son ID.
|
|
101
|
-
*
|
|
102
|
-
* @param {string} entityType - Le type d'entité.
|
|
103
|
-
* @param {string} entityId - L'identifiant de l'entité.
|
|
104
|
-
* @param {Object} [options] - Options supplémentaires :
|
|
105
|
-
* @param {boolean} [options.skipGet] - Si true, ne pas appeler `get()` sur l'entité.
|
|
106
|
-
* @return {Promise<Object|null>} L'entité liée ou null.
|
|
107
|
-
*/
|
|
108
|
-
async linkEntityById(entityType, entityId, options = {}) {
|
|
109
|
-
const meta = this._getEntityMeta(entityType);
|
|
110
|
-
if (!meta) return null;
|
|
111
|
-
const entity = new meta.entityClass(this, { id: entityId }, meta.deps);
|
|
112
|
-
if(options?.skipGet) return entity;
|
|
113
|
-
await entity.get();
|
|
114
|
-
return entity;
|
|
115
|
-
},
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Lie des entités présentes dans `this.serverData` à partir de leurs IDs,
|
|
120
|
-
* en les filtrant dynamiquement et en optionnellement les transformant.
|
|
121
|
-
*
|
|
122
|
-
* @param {string} entityType - Le type d'entité (ex : "badges", "citoyens", etc.).
|
|
123
|
-
* @param {Object} filters - Clés/valeurs de filtres dynamiques. Les valeurs peuvent être :
|
|
124
|
-
* - un littéral (comparaison stricte ou intelligente selon le type),
|
|
125
|
-
* - une chaîne (utilise `includes` insensible à la casse),
|
|
126
|
-
* - une RegExp (appliquée si la valeur est une chaîne),
|
|
127
|
-
* - une fonction `(value) => boolean`.
|
|
128
|
-
* @param {Object} [options] - Options supplémentaires :
|
|
129
|
-
* @param {string} [options.key] - Le champ de `this.serverData` à utiliser (par défaut = entityType).
|
|
130
|
-
* @param {Function} [options.mapFn] - Fonction de transformation `(entity) => any` appliquée au résultat.
|
|
131
|
-
*
|
|
132
|
-
* @return {Promise<Array<Object>>} Liste des entités liées, filtrées et éventuellement transformées.
|
|
133
|
-
*
|
|
134
|
-
* @example
|
|
135
|
-
* // Tous les badges avec `name` contenant "codev"
|
|
136
|
-
* const badges = await this.linkEntitiesFromServerData("badges", { name: "codev" });
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* // Badges non expirés et visibles
|
|
140
|
-
* const badges = await this.linkEntitiesFromServerData("badges", {
|
|
141
|
-
* expiredOn: false,
|
|
142
|
-
* show: "true"
|
|
143
|
-
* });
|
|
144
|
-
*
|
|
145
|
-
* @example
|
|
146
|
-
* // Badges émis après 2023
|
|
147
|
-
* const badges = await this.linkEntitiesFromServerData("badges", {
|
|
148
|
-
* issuedOn: (v) => new Date(v) >= new Date("2023-01-01")
|
|
149
|
-
* });
|
|
150
|
-
*
|
|
151
|
-
* @example
|
|
152
|
-
* // Extraire uniquement les noms des badges
|
|
153
|
-
* const namesOnly = await this.linkEntitiesFromServerData("badges", {}, {
|
|
154
|
-
* mapFn: (badge) => badge.meta.name
|
|
155
|
-
* });
|
|
156
|
-
*/
|
|
157
|
-
async linkEntitiesFromServerData(entityType, filters = {}, options = {}) {
|
|
158
|
-
const key = options.key || entityType;
|
|
159
|
-
const mapFn = typeof options.mapFn === "function" ? options.mapFn : null;
|
|
160
|
-
|
|
161
|
-
const isTruthy = (v) => v === true || v === "true";
|
|
162
|
-
const isFalsy = (v) => v === false || v === "false";
|
|
163
|
-
|
|
164
|
-
const data = this.serverData?.[key];
|
|
165
|
-
const result = [];
|
|
166
|
-
|
|
167
|
-
if (data && typeof data === "object" && Object.keys(data).length > 0) {
|
|
168
|
-
for (const id of Object.keys(data)) {
|
|
169
|
-
const meta = data[id];
|
|
170
|
-
|
|
171
|
-
const matches = Object.entries(filters).every(([key, expected]) => {
|
|
172
|
-
const actual = meta[key];
|
|
173
|
-
|
|
174
|
-
if (typeof expected === "function") {
|
|
175
|
-
try {
|
|
176
|
-
return expected(actual);
|
|
177
|
-
} catch (err) {
|
|
178
|
-
console.warn(`Erreur dans le filtre personnalisé pour ${key}`, err);
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (expected instanceof RegExp) {
|
|
184
|
-
return typeof actual === "string" && expected.test(actual);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (typeof expected === "string" && typeof actual === "string") {
|
|
188
|
-
return actual.toLowerCase().includes(expected.toLowerCase());
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (isTruthy(expected)) return isTruthy(actual);
|
|
192
|
-
if (isFalsy(expected)) return isFalsy(actual);
|
|
193
|
-
|
|
194
|
-
if (
|
|
195
|
-
typeof actual === "string" &&
|
|
196
|
-
typeof expected === "string" &&
|
|
197
|
-
!isNaN(Date.parse(actual)) &&
|
|
198
|
-
!isNaN(Date.parse(expected))
|
|
199
|
-
) {
|
|
200
|
-
return new Date(actual).getTime() === new Date(expected).getTime();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return actual === expected;
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (!matches) continue;
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const entity = await this.linkEntityById(entityType, id);
|
|
210
|
-
if (entity) {
|
|
211
|
-
entity.meta = meta;
|
|
212
|
-
result.push(mapFn ? mapFn(entity) : entity);
|
|
213
|
-
}
|
|
214
|
-
} catch (error) {
|
|
215
|
-
this._logger?.error?.(`Erreur lors de la récupération de l'entité ${entityType} (${id}) :`, error);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return result;
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
entityInstanceData(entityType, entityData) {
|
|
224
|
-
const meta = this._getEntityMeta(entityType);
|
|
225
|
-
const selfKey = this.__entityTag; // ex: "User", "Organization", etc.
|
|
226
|
-
const selfClass = this.constructor;
|
|
227
|
-
|
|
228
|
-
// pour citoyens la signature est différentes
|
|
229
|
-
return new meta.entityClass(this, entityData, {
|
|
230
|
-
[selfKey]: selfClass,
|
|
231
|
-
...meta.deps
|
|
232
|
-
});
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Crée une instance d'entité, et déclenche get() si certaines propriétés sont présentes.
|
|
237
|
-
* TODO : pas finis
|
|
238
|
-
* @param {string} entityType
|
|
239
|
-
* @param {Object} entityData
|
|
240
|
-
* @return {Promise<Object>}
|
|
241
|
-
*/
|
|
242
|
-
async entity(entityType, entityData = {}) {
|
|
243
|
-
try {
|
|
244
|
-
const entity = this.entityInstanceData(entityType, entityData);
|
|
245
|
-
|
|
246
|
-
const fetchKeysByEntity = {
|
|
247
|
-
citoyens: ["id", "slug"],
|
|
248
|
-
organisations: ["id", "slug"],
|
|
249
|
-
projets: ["id", "slug"],
|
|
250
|
-
events: ["id", "slug"],
|
|
251
|
-
poi: ["id", "slug"],
|
|
252
|
-
news: ["id"],
|
|
253
|
-
badges: ["id"],
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
const fetchKeys = fetchKeysByEntity[entityType];
|
|
257
|
-
|
|
258
|
-
if (fetchKeys && this._hasAtLeastOne(entityData, fetchKeys)) {
|
|
259
|
-
await entity.get();
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return entity;
|
|
263
|
-
} catch (error) {
|
|
264
|
-
this.apiClient._logger.error(`[Api.${this.__entityTag}.${entityType}] Erreur lors de la création d'une instance ${entityType} :`, error.message);
|
|
265
|
-
throw error;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
};
|
package/src/mixin/UtilMixin.js
DELETED
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
import ObjectID from "bson-objectid";
|
|
2
|
-
// import { fileTypeFromBuffer } from "file-type";
|
|
3
|
-
import pkg from "file-type";
|
|
4
|
-
const { fromBuffer } = pkg;
|
|
5
|
-
|
|
6
|
-
import { ApiAuthenticationError, ApiValidationError } from "../error.js";
|
|
7
|
-
|
|
8
|
-
// UtilMixin.js
|
|
9
|
-
export const UtilMixin = {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Appelle une méthode de l'API.
|
|
13
|
-
*
|
|
14
|
-
* @param {string} constant - Le nom de la méthode à appeler.
|
|
15
|
-
* @param {object} data - Les données à passer à la méthode.
|
|
16
|
-
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
17
|
-
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
18
|
-
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
19
|
-
* @throws {ApiClientError} - Si l'utilisateur n'est pas authentifié.
|
|
20
|
-
*/
|
|
21
|
-
async call(constant, data = {}) {
|
|
22
|
-
return this.apiClient.safeCall(async () => {
|
|
23
|
-
const response = await this.apiClient.callEndpoint(constant, data);
|
|
24
|
-
this.apiClient.checkAndThrowApiResponseError(response);
|
|
25
|
-
return response.data;
|
|
26
|
-
});
|
|
27
|
-
},
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Appelle une méthode de l'API si l'utilisateur n'est pas connecté.
|
|
31
|
-
*
|
|
32
|
-
* @param {string} constant - Le nom de la méthode à appeler.
|
|
33
|
-
* @param {object} data - Les données à passer à la méthode.
|
|
34
|
-
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
35
|
-
* @throws {ApiAuthenticationError} - Si l'utilisateur est connecté.
|
|
36
|
-
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
37
|
-
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
38
|
-
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
39
|
-
*/
|
|
40
|
-
async callNoConnected(constant, data = {}) {
|
|
41
|
-
if(this.isConnected) {
|
|
42
|
-
throw new ApiAuthenticationError("Vous devez ne devez pas être connecté pour faire cette action.");
|
|
43
|
-
}
|
|
44
|
-
return this.call(constant, data);
|
|
45
|
-
},
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Appelle une méthode de l'API si l'utilisateur est connecté.
|
|
49
|
-
*
|
|
50
|
-
* @param {string|function} param - Le nom de la méthode à appeler ou une fonction de rappel.
|
|
51
|
-
* @param {object} data - Les données à passer à la méthode.
|
|
52
|
-
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
53
|
-
* @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas connecté.
|
|
54
|
-
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
55
|
-
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
56
|
-
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
57
|
-
*/
|
|
58
|
-
async callIsConnected(param, data = {}) {
|
|
59
|
-
if(!this.isConnected) {
|
|
60
|
-
throw new ApiAuthenticationError("Vous devez être connecté pour faire cette action.");
|
|
61
|
-
}
|
|
62
|
-
// Si le premier paramètre est une fonction, on l'exécute en tant que callback
|
|
63
|
-
if (typeof param === "function") {
|
|
64
|
-
return await param();
|
|
65
|
-
}
|
|
66
|
-
return this.call(param, data);
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Appelle une méthode de l'API si l'utilisateur est lui-même.
|
|
71
|
-
*
|
|
72
|
-
* @param {string|function} param - Le nom de la méthode à appeler ou une fonction de rappel.
|
|
73
|
-
* @param {object} data - Les données à passer à la méthode.
|
|
74
|
-
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
75
|
-
* @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas lui-même.
|
|
76
|
-
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
77
|
-
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
78
|
-
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
79
|
-
*/
|
|
80
|
-
async callIsMe(param, data = {}) {
|
|
81
|
-
if (!this.isMe) {
|
|
82
|
-
throw new ApiAuthenticationError("Vous devez être vous-même pour faire cette action.");
|
|
83
|
-
}
|
|
84
|
-
// Si le premier paramètre est une fonction, on l'exécute en tant que callback
|
|
85
|
-
if (typeof param === "function") {
|
|
86
|
-
return await param();
|
|
87
|
-
}
|
|
88
|
-
// Sinon, on considère qu'il s'agit d'un constant et on appelle la méthode par défaut
|
|
89
|
-
return await this.callIsConnected(param, data);
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
async _validateImage(imageInput){
|
|
93
|
-
const image = await this._validateUploadInput(imageInput, {
|
|
94
|
-
allowedMimeTypes: ["image/jpeg", "image/png", "image/jpg"],
|
|
95
|
-
expectedType: "image"
|
|
96
|
-
});
|
|
97
|
-
return image;
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
async _validateFile(fileInput){
|
|
101
|
-
const file = await this._validateUploadInput(fileInput, {
|
|
102
|
-
allowedMimeTypes: ["application/pdf", "text/plain", "text/csv"],
|
|
103
|
-
expectedType: "file"
|
|
104
|
-
});
|
|
105
|
-
return file;
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
async _validateUploadInput(input, { allowedMimeTypes = [], expectedType = "any" }) {
|
|
109
|
-
if (!input) {
|
|
110
|
-
throw new ApiValidationError("Le fichier est requis.");
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const isNode = typeof window === "undefined" && typeof process !== "undefined";
|
|
114
|
-
let mimeType = "";
|
|
115
|
-
let output = input;
|
|
116
|
-
|
|
117
|
-
// Navigateur : File
|
|
118
|
-
if (typeof File !== "undefined" && input instanceof File) {
|
|
119
|
-
mimeType = input.type;
|
|
120
|
-
if (!allowedMimeTypes.includes(mimeType)) {
|
|
121
|
-
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Navigateur : Blob
|
|
126
|
-
else if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
127
|
-
mimeType = input.type;
|
|
128
|
-
if (!allowedMimeTypes.includes(mimeType)) {
|
|
129
|
-
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const ext = mimeType.split("/")[1] || "bin";
|
|
133
|
-
const fileName = `${Date.now()}.${ext}`;
|
|
134
|
-
output = new File([input], fileName, { type: mimeType });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Node.js : Buffer
|
|
138
|
-
else if (isNode && Buffer.isBuffer(input)) {
|
|
139
|
-
const fileTypeResult = await fromBuffer(input);
|
|
140
|
-
mimeType = fileTypeResult?.mime;
|
|
141
|
-
|
|
142
|
-
if (!fileTypeResult || !allowedMimeTypes.includes(mimeType)) {
|
|
143
|
-
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Pour un fichier image, on transforme en stream
|
|
147
|
-
if (expectedType === "image") {
|
|
148
|
-
const ext = fileTypeResult.ext;
|
|
149
|
-
const filename = `${Date.now()}.${ext}`;
|
|
150
|
-
output = await this._createReadStreamFromBuffer(input, filename, mimeType);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Node.js : ReadableStream
|
|
155
|
-
else if (isNode && input?.readable && typeof input._read === "function") {
|
|
156
|
-
const previewChunks = [];
|
|
157
|
-
const tee = await this._passThrough();
|
|
158
|
-
const resultStream = await this._passThrough();
|
|
159
|
-
|
|
160
|
-
const MAX_BYTES = 4100;
|
|
161
|
-
let bytesRead = 0;
|
|
162
|
-
|
|
163
|
-
input.on("data", (chunk) => {
|
|
164
|
-
if (bytesRead < MAX_BYTES) {
|
|
165
|
-
previewChunks.push(chunk);
|
|
166
|
-
bytesRead += chunk.length;
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
input.pipe(tee).pipe(resultStream);
|
|
171
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
172
|
-
|
|
173
|
-
const previewBuffer = Buffer.concat(previewChunks);
|
|
174
|
-
const fileTypeResult = await fromBuffer(previewBuffer);
|
|
175
|
-
mimeType = fileTypeResult?.mime;
|
|
176
|
-
|
|
177
|
-
if (!fileTypeResult || !allowedMimeTypes.includes(mimeType)) {
|
|
178
|
-
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
resultStream.path = `${Date.now()}.${fileTypeResult.ext}`;
|
|
182
|
-
resultStream.mimeType = mimeType;
|
|
183
|
-
output = resultStream;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
else {
|
|
187
|
-
throw new ApiValidationError("Type de fichier non reconnu.");
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return output;
|
|
191
|
-
},
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Transforme un Buffer en ReadableStream équivalent à fs.createReadStream
|
|
195
|
-
* @param {Buffer} buffer - Le buffer contenant les données binaires
|
|
196
|
-
* @param {string} filename - Nom de fichier (utilisé dans FormData)
|
|
197
|
-
* @param {string} mimeType - Type MIME (utilisé dans FormData)
|
|
198
|
-
* @returns {Object} - { stream, filename, mimeType }
|
|
199
|
-
*/
|
|
200
|
-
async _createReadStreamFromBuffer(buffer, filename = "file.bin", mimeType = "application/octet-stream") {
|
|
201
|
-
const stream = await this._bufferToReadable(buffer);
|
|
202
|
-
stream.path = filename; // 👈 hack pour simuler un vrai fichier ReadStream
|
|
203
|
-
stream.mimeType = mimeType;
|
|
204
|
-
return stream;
|
|
205
|
-
},
|
|
206
|
-
|
|
207
|
-
async _bufferToReadable(buffer) {
|
|
208
|
-
if (typeof window === "undefined") {
|
|
209
|
-
const { bufferToReadable } = await import("../utils/stream-utils.node.js");
|
|
210
|
-
return bufferToReadable(buffer);
|
|
211
|
-
} else {
|
|
212
|
-
throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
async _passThrough() {
|
|
217
|
-
if (typeof window === "undefined") {
|
|
218
|
-
const { createPassThrough } = await import("../utils/stream-utils.node.js");
|
|
219
|
-
return createPassThrough();
|
|
220
|
-
} else {
|
|
221
|
-
throw new Error("passThrough ne doit pas être appelé dans le navigateur");
|
|
222
|
-
}
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
// async _bufferToReadable(buffer) {
|
|
226
|
-
// if (typeof window === "undefined") {
|
|
227
|
-
// const { Readable } = await import("stream");
|
|
228
|
-
// return Readable.from(buffer);
|
|
229
|
-
// } else {
|
|
230
|
-
// throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
|
|
231
|
-
// }
|
|
232
|
-
// },
|
|
233
|
-
|
|
234
|
-
// async _passThrough() {
|
|
235
|
-
// if (typeof window === "undefined") {
|
|
236
|
-
// const { PassThrough } = await import("stream");
|
|
237
|
-
// return new PassThrough();
|
|
238
|
-
// } else {
|
|
239
|
-
// throw new Error("passThrough ne doit pas être appelé dans le navigateur");
|
|
240
|
-
// }
|
|
241
|
-
// },
|
|
242
|
-
|
|
243
|
-
_omitProps(obj, propsToRemove) {
|
|
244
|
-
if (!obj || typeof obj !== "object") return {};
|
|
245
|
-
|
|
246
|
-
const result = { ...obj };
|
|
247
|
-
for (const prop of propsToRemove) {
|
|
248
|
-
delete result[prop];
|
|
249
|
-
}
|
|
250
|
-
return result;
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
_pickProps(obj, keys, transforms = {}) {
|
|
254
|
-
if (!obj || typeof obj !== "object") return {};
|
|
255
|
-
|
|
256
|
-
const result = {};
|
|
257
|
-
|
|
258
|
-
for (const key of keys) {
|
|
259
|
-
if (key in obj) {
|
|
260
|
-
const value = obj[key];
|
|
261
|
-
if (typeof transforms[key] === "function") {
|
|
262
|
-
result[key] = transforms[key](value, obj); // (valeur, objet source)
|
|
263
|
-
} else {
|
|
264
|
-
result[key] = value;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return result;
|
|
270
|
-
},
|
|
271
|
-
|
|
272
|
-
_hasAtLeastOne(obj, keys = []) {
|
|
273
|
-
return keys.some((key) => key in obj && obj[key] != null);
|
|
274
|
-
},
|
|
275
|
-
|
|
276
|
-
_createFilteredProxy(list) {
|
|
277
|
-
return new Proxy(list, {
|
|
278
|
-
get(target, prop, receiver) {
|
|
279
|
-
if (typeof prop === "string" && !isNaN(prop)) {
|
|
280
|
-
const active = target.filter(n => !n._isDeleted);
|
|
281
|
-
return active[prop];
|
|
282
|
-
}
|
|
283
|
-
if (prop === "length") {
|
|
284
|
-
return target.filter(n => !n._isDeleted).length;
|
|
285
|
-
}
|
|
286
|
-
if (typeof target[prop] === "function") {
|
|
287
|
-
return (...args) => target.filter(n => !n._isDeleted)[prop](...args);
|
|
288
|
-
}
|
|
289
|
-
return Reflect.get(target, prop, receiver);
|
|
290
|
-
}
|
|
291
|
-
});
|
|
292
|
-
},
|
|
293
|
-
|
|
294
|
-
_newId() {
|
|
295
|
-
const newId = new ObjectID();
|
|
296
|
-
return newId.toString();
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
};
|
|
300
|
-
|