@communecter/cocolight-api-client 1.0.8 → 1.0.10
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/22.cocolight-api-client.mjs.js +1 -0
- package/dist/320.cocolight-api-client.browser.js +1 -0
- package/dist/931.cocolight-api-client.browser.js +1 -0
- package/dist/931.cocolight-api-client.cjs +1 -0
- package/dist/cocolight-api-client.browser.js +3 -3
- package/dist/cocolight-api-client.cjs +1 -2
- package/dist/cocolight-api-client.mjs.js +1 -2
- package/package.json +6 -3
- package/src/Api.js +36 -19
- package/src/ApiClient.js +56 -11
- package/src/api/EndpointApi.js +1534 -0
- package/src/api/News.js +327 -0
- package/src/api/Organization.js +268 -84
- package/src/api/Project.js +287 -93
- package/src/api/User.js +397 -87
- package/src/api/UserApi.js +5 -1
- package/src/endpoints.module.js +2 -2
- package/src/mixin/DraftStateMixin.js +176 -0
- package/src/mixin/EntityMixin.js +109 -0
- package/src/mixin/MutualEntityMixin.js +48 -0
- package/src/mixin/NewsMixin.js +42 -0
- package/src/mixin/UserMixin.js +8 -0
- package/src/mixin/UtilMixin.js +294 -0
- package/src/utils/stream-utils.node.js +10 -0
- package/dist/cocolight-api-client.cjs.LICENSE.txt +0 -1
- package/dist/cocolight-api-client.mjs.js.LICENSE.txt +0 -1
- package/src/api/EntityMixin.js +0 -249
- package/src/api/NewsMixin.js +0 -168
- package/src/api/UserMixin.js +0 -59
- package/src/api/UtilMixin.js +0 -82
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// DraftStateMixin.js
|
|
2
|
+
import { ApiError, ApiValidationError } from "../error.js";
|
|
3
|
+
|
|
4
|
+
export const DraftStateMixin = {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Crée un proxy combinant draft + serveur, avec transformations facultatives.
|
|
8
|
+
* @param {Object} server
|
|
9
|
+
* @param {Object} draft
|
|
10
|
+
* @param {Array} allowedFields - champs autorisés dans le draft
|
|
11
|
+
* @param {Object} [transforms={}] - transformateurs de lecture
|
|
12
|
+
* @param {Object} [options={}] - options
|
|
13
|
+
* @returns {Proxy}
|
|
14
|
+
*/
|
|
15
|
+
createDraftProxy(apiClient, server = {}, draft = {}, allowedFields = [], transforms = {}, options = {}) {
|
|
16
|
+
return new Proxy({}, {
|
|
17
|
+
get: (_, prop) => {
|
|
18
|
+
const val = prop in draft ? draft[prop] : server[prop];
|
|
19
|
+
const transformer = transforms[prop];
|
|
20
|
+
return typeof transformer === "function" ? transformer(val) : val;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
set: (_, prop, value) => {
|
|
24
|
+
if (!allowedFields.includes(prop)) {
|
|
25
|
+
const message = `[DraftProxy] Le champ "${prop}" n'est pas autorisé.`;
|
|
26
|
+
if (options.throwOnError) {
|
|
27
|
+
throw new ApiValidationError(message, 400, null, {
|
|
28
|
+
field: prop,
|
|
29
|
+
value,
|
|
30
|
+
allowedFields
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
apiClient._logger.warn(message);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
draft[prop] = value;
|
|
37
|
+
return true;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
deleteProperty: (_, prop) => {
|
|
41
|
+
if (!allowedFields.includes(prop)) return false;
|
|
42
|
+
delete draft[prop];
|
|
43
|
+
return true;
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
has: (_, prop) => prop in draft || prop in server,
|
|
47
|
+
ownKeys: () => [...new Set([...Object.keys(server), ...Object.keys(draft)])],
|
|
48
|
+
getOwnPropertyDescriptor: () => ({ enumerable: true, configurable: true })
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
_extractWritableFields(schema = {}, data = {}, ctx = { defs: {}, visited: new Set() }) {
|
|
53
|
+
if (!schema || typeof schema !== "object") return [];
|
|
54
|
+
if (schema.$id && ctx.visited.has(schema.$id)) return [];
|
|
55
|
+
if (schema.$id) ctx.visited.add(schema.$id);
|
|
56
|
+
ctx.defs = schema.$defs || schema.definitions || ctx.defs;
|
|
57
|
+
|
|
58
|
+
const fields = [];
|
|
59
|
+
|
|
60
|
+
if (schema.$ref) {
|
|
61
|
+
const refKey = schema.$ref.replace(/^#\/?(\$defs|definitions)\//, "");
|
|
62
|
+
const resolved = ctx.defs?.[refKey];
|
|
63
|
+
if (resolved) fields.push(...this._extractWritableFields(resolved, data, ctx));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (schema.allOf) {
|
|
67
|
+
schema.allOf.forEach(s => fields.push(...this._extractWritableFields(s, data, ctx)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (schema.if && schema.then) {
|
|
71
|
+
const condition = schema.if?.properties;
|
|
72
|
+
let matches = true;
|
|
73
|
+
if (condition) {
|
|
74
|
+
for (const key in condition) {
|
|
75
|
+
const expected = condition[key]?.const;
|
|
76
|
+
if (data[key] !== expected) {
|
|
77
|
+
matches = false;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (matches && schema.then) {
|
|
83
|
+
fields.push(...this._extractWritableFields(schema.then, data, ctx));
|
|
84
|
+
} else if (!matches && schema.else) {
|
|
85
|
+
fields.push(...this._extractWritableFields(schema.else, data, ctx));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (schema.properties) {
|
|
90
|
+
fields.push(...Object.entries(schema.properties)
|
|
91
|
+
// eslint-disable-next-line no-unused-vars
|
|
92
|
+
.filter(([_, def]) => def.readOnly !== true && def.const === undefined)
|
|
93
|
+
.map(([key]) => key));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return [...new Set(fields)];
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
buildDraftAndProxy({ data = {}, serverData = null, constant, apiClient, transforms = {}, throwOnError = true, removeFields = [] }) {
|
|
100
|
+
const constants = Array.isArray(constant) ? constant : [constant];
|
|
101
|
+
const combinedSchema = {
|
|
102
|
+
allOf: [],
|
|
103
|
+
$defs: {}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
for (const key of constants) {
|
|
107
|
+
const sch = apiClient.getRequestSchema(key);
|
|
108
|
+
if (!sch) throw new ApiError(`Unable to find schema for ${key}.`);
|
|
109
|
+
|
|
110
|
+
// Extraire et fusionner les $defs
|
|
111
|
+
if (sch.$defs) {
|
|
112
|
+
for (const [defKey, defVal] of Object.entries(sch.$defs)) {
|
|
113
|
+
if (combinedSchema.$defs[defKey]) {
|
|
114
|
+
apiClient._logger.warn(`Duplicate $defs key '${defKey}' from schema '${key}'`);
|
|
115
|
+
} else {
|
|
116
|
+
combinedSchema.$defs[defKey] = defVal;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
combinedSchema.allOf.push(sch);
|
|
122
|
+
}
|
|
123
|
+
const draft = {};
|
|
124
|
+
let allowed = this._extractWritableFields(combinedSchema, data);
|
|
125
|
+
|
|
126
|
+
if (data.id && allowed.indexOf("id") === -1) {
|
|
127
|
+
allowed.push("id");
|
|
128
|
+
}
|
|
129
|
+
if (data.slug && allowed.indexOf("slug") === -1) {
|
|
130
|
+
allowed.push("slug");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
allowed = allowed.filter(k => !removeFields.includes(k));
|
|
134
|
+
|
|
135
|
+
for (const key of allowed) {
|
|
136
|
+
const raw = data[key];
|
|
137
|
+
const transformed = typeof transforms[key] === "function" ? transforms[key](raw, data) : raw;
|
|
138
|
+
if (transformed !== undefined) draft[key] = transformed;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const proxy = this.createDraftProxy(apiClient, serverData, draft, allowed, transforms, { throwOnError });
|
|
142
|
+
|
|
143
|
+
return { draft, proxy };
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
extractChangedFieldsFromSchema(apiClient, constant, data = {}, getInitialDraft, removeFields = []) {
|
|
147
|
+
const schema = apiClient.getRequestSchema(constant);
|
|
148
|
+
let allowed = this._extractWritableFields(schema, data);
|
|
149
|
+
const changed = {};
|
|
150
|
+
const initialDraft = getInitialDraft?.() || {};
|
|
151
|
+
|
|
152
|
+
// on enlève les champs qui ne sont pas dans le draft
|
|
153
|
+
// ou qui sont dans removeFields
|
|
154
|
+
allowed = allowed.filter(k => !removeFields.includes(k));
|
|
155
|
+
|
|
156
|
+
for (const key of allowed) {
|
|
157
|
+
// on verifie que le champ existe dans le draft
|
|
158
|
+
// sinon on ne le prend pas en compte
|
|
159
|
+
|
|
160
|
+
if (data[key] === undefined) continue;
|
|
161
|
+
|
|
162
|
+
const current = data[key];
|
|
163
|
+
const initial = initialDraft[key];
|
|
164
|
+
|
|
165
|
+
const changedValue =
|
|
166
|
+
JSON.stringify(current) !== JSON.stringify(initial);
|
|
167
|
+
|
|
168
|
+
if (changedValue) {
|
|
169
|
+
changed[key] = current;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Object.keys(changed).length > 0 ? changed : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ApiResponseError } from "../error.js";
|
|
2
|
+
|
|
3
|
+
// EntityMixin.js
|
|
4
|
+
export const EntityMixin = {
|
|
5
|
+
/**
|
|
6
|
+
* Récupère le profil complet de l'organisation.
|
|
7
|
+
*
|
|
8
|
+
* @returns {Promise<Object>} Le profil complet.
|
|
9
|
+
*/
|
|
10
|
+
async get() {
|
|
11
|
+
return this.apiClient.safeCall(async () => {
|
|
12
|
+
const data = await this.getPublicProfile();
|
|
13
|
+
this._setData(data);
|
|
14
|
+
return data;
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Mettre à jour les paramètres d'un élément : Mise à jour des paramètres spécifiques d'un élément.
|
|
20
|
+
* Constant : UPDATE_SETTINGS
|
|
21
|
+
* @param {Object} data - Les données à envoyer.
|
|
22
|
+
* @param {string} data.type - data.type
|
|
23
|
+
* @param {undefined} data.value - data.value
|
|
24
|
+
* @returns {Promise<Object>} - Les données de réponse.
|
|
25
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
26
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
27
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
28
|
+
*/
|
|
29
|
+
async updateSettings(data = {}) {
|
|
30
|
+
data.idEntity = this.id;
|
|
31
|
+
data.typeEntity = this.getEntityType();
|
|
32
|
+
return this.callIsConnected(() => this.endpointApi.updateSettings(data));
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Mettre à jour la description d'un élément : Permet de mettre à jour la description courte et complète d'un élément.
|
|
37
|
+
* Constant : UPDATE_BLOCK_DESCRIPTION
|
|
38
|
+
* @param {Object} data - Les données à envoyer.
|
|
39
|
+
* @param {string} data.descMentions - Mentions dans la description (default: "")
|
|
40
|
+
* @param {string} data.shortDescription - Courte description
|
|
41
|
+
* @param {string} data.description - Description complète
|
|
42
|
+
* @returns {Promise<Object>} - Les données de réponse.
|
|
43
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
44
|
+
* @throws {ApiAuthenticationError} - En cas d'erreur d'authentification.
|
|
45
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
46
|
+
*/
|
|
47
|
+
async updateDescription(data = {}) {
|
|
48
|
+
data.typeElement = this.getEntityType();
|
|
49
|
+
data.id = this.id;
|
|
50
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockDescription(data));
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mettre à jour les informations d'un élément : Permet de mettre à jour les informations générales d'un élément (nom, contacts, etc.).
|
|
55
|
+
* Constant : UPDATE_BLOCK_INFO
|
|
56
|
+
*/
|
|
57
|
+
async updateInfo(data = {}) {
|
|
58
|
+
data.typeElement = this.getEntityType();
|
|
59
|
+
data.id = this.id;
|
|
60
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockInfo(data));
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 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.
|
|
65
|
+
* Constant : UPDATE_BLOCK_SOCIAL
|
|
66
|
+
*/
|
|
67
|
+
async updateSocial(data = {}) {
|
|
68
|
+
data.typeElement = this.getEntityType();
|
|
69
|
+
data.id = this.id;
|
|
70
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockSocial(data));
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Mettre à jour les localités d'un élément : Permet de mettre à jour l'adresse et les informations géographiques d'un élément.
|
|
75
|
+
* Constant : UPDATE_BLOCK_LOCALITY
|
|
76
|
+
*/
|
|
77
|
+
async updateLocality(data = {}) {
|
|
78
|
+
data.typeElement = this.getEntityType();
|
|
79
|
+
data.id = this.id;
|
|
80
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockLocality(data));
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Mettre à jour le slug d'un élément : Permet de mettre à jour le slug pour une URL simplifiée.
|
|
85
|
+
* Constant : UPDATE_BLOCK_SLUG
|
|
86
|
+
*/
|
|
87
|
+
async updateSlug({ slug }) {
|
|
88
|
+
try {
|
|
89
|
+
await this.endpointApi.check({ type: this.getEntityType(), id: this.id, slug });
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if(error instanceof ApiResponseError) {
|
|
92
|
+
throw new ApiResponseError("Erreur lors de la vérification du slug.", error.status, error.data);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
return this.callIsConnected(() => this.endpointApi.updateBlockSlug({ typeElement: this.getEntityType(), id: this.id, slug }));
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Mettre à jour l'image de profil : Permet de mettre à jour l'image de profil d'un utilisateur ou d'une entité.
|
|
101
|
+
* Constant : PROFIL_IMAGE
|
|
102
|
+
*/
|
|
103
|
+
async updateImageProfil({ profil_avatar: image }) {
|
|
104
|
+
image = await this.validateImage(image);
|
|
105
|
+
const data = { pathParams: { folder: this.getEntityType(), ownerId: this.id }, profil_avatar: image };
|
|
106
|
+
return this.callIsConnected(() => this.endpointApi.profilImage(data));
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ApiResponseError } from "../error.js";
|
|
2
|
+
|
|
3
|
+
// NewsMixin.js
|
|
4
|
+
export const NewsMixin = {
|
|
5
|
+
/**
|
|
6
|
+
* Récupérer les actualités : Récupère la liste d’actualités selon plusieurs critères.
|
|
7
|
+
* Constant : GET_NEWS
|
|
8
|
+
* @param {Object} data - Les données à envoyer.
|
|
9
|
+
* @param {number} data.dateLimit - Limite de date timestamp ou 0 (default: 0)
|
|
10
|
+
* @param {object} data.search - data.search
|
|
11
|
+
* @param {string} data.search.name - Nom ou terme recherché (default: "")
|
|
12
|
+
* @param {number} data.indexStep - Nombre de résultats par page (default: 12)
|
|
13
|
+
* @returns {Promise<Object>} - Les données de réponse.
|
|
14
|
+
* @throws {ApiResponseError} - En cas d'erreur détectée dans la réponse.
|
|
15
|
+
* @throws {Error} - En cas d'erreur inattendue.
|
|
16
|
+
*/
|
|
17
|
+
async getNews(data = {}) {
|
|
18
|
+
data.pathParams = { type: this.getEntityType(), id: this.id };
|
|
19
|
+
const arrayObjetNews = await this.endpointApi.getNews(data);
|
|
20
|
+
if(!Array.isArray(arrayObjetNews)){
|
|
21
|
+
throw new ApiResponseError("Erreur lors de la récupération des actualités.", 500, arrayObjetNews);
|
|
22
|
+
}
|
|
23
|
+
const rawNewsList = arrayObjetNews.map((newsData) =>
|
|
24
|
+
this.deps.News.fromServerData(newsData, this, { User : this.deps.User, EndpointApi: this.deps.EndpointApi })
|
|
25
|
+
);
|
|
26
|
+
return this._createFilteredProxy(rawNewsList);
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async news(newsData) {
|
|
30
|
+
try {
|
|
31
|
+
const news = new this.deps.News(this, newsData, { User: this.deps.User, EndpointApi : this.deps.EndpointApi });
|
|
32
|
+
if (newsData.id) {
|
|
33
|
+
await news.get();
|
|
34
|
+
}
|
|
35
|
+
return news;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
this.apiClient._logger.error(`[Api.${this.getEntityType()}.news] Erreur lors de la création d'une instance news :`, error.message);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import ObjectID from "bson-objectid";
|
|
2
|
+
import { fileTypeFromBuffer } from "file-type";
|
|
3
|
+
|
|
4
|
+
import { ApiAuthenticationError, ApiValidationError } from "../error.js";
|
|
5
|
+
|
|
6
|
+
// UtilMixin.js
|
|
7
|
+
export const UtilMixin = {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Appelle une méthode de l'API.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} constant - Le nom de la méthode à appeler.
|
|
13
|
+
* @param {object} data - Les données à passer à la méthode.
|
|
14
|
+
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
15
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
16
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
17
|
+
* @throws {ApiClientError} - Si l'utilisateur n'est pas authentifié.
|
|
18
|
+
*/
|
|
19
|
+
async call(constant, data = {}) {
|
|
20
|
+
return this.apiClient.safeCall(async () => {
|
|
21
|
+
const response = await this.apiClient.callEndpoint(constant, data);
|
|
22
|
+
this.apiClient.checkAndThrowApiResponseError(response);
|
|
23
|
+
return response.data;
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Appelle une méthode de l'API si l'utilisateur n'est pas connecté.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} constant - Le nom de la méthode à appeler.
|
|
31
|
+
* @param {object} data - Les données à passer à la méthode.
|
|
32
|
+
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
33
|
+
* @throws {ApiAuthenticationError} - Si l'utilisateur est connecté.
|
|
34
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
35
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
36
|
+
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
37
|
+
*/
|
|
38
|
+
async callNoConnected(constant, data = {}) {
|
|
39
|
+
if(this.isConnected) {
|
|
40
|
+
throw new ApiAuthenticationError("Vous devez ne devez pas être connecté pour faire cette action.");
|
|
41
|
+
}
|
|
42
|
+
return this.call(constant, data);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Appelle une méthode de l'API si l'utilisateur est connecté.
|
|
47
|
+
*
|
|
48
|
+
* @param {string|function} param - Le nom de la méthode à appeler ou une fonction de rappel.
|
|
49
|
+
* @param {object} data - Les données à passer à la méthode.
|
|
50
|
+
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
51
|
+
* @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas connecté.
|
|
52
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
53
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
54
|
+
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
55
|
+
*/
|
|
56
|
+
async callIsConnected(param, data = {}) {
|
|
57
|
+
if(!this.isConnected) {
|
|
58
|
+
throw new ApiAuthenticationError("Vous devez être connecté pour faire cette action.");
|
|
59
|
+
}
|
|
60
|
+
// Si le premier paramètre est une fonction, on l'exécute en tant que callback
|
|
61
|
+
if (typeof param === "function") {
|
|
62
|
+
return await param();
|
|
63
|
+
}
|
|
64
|
+
return this.call(param, data);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Appelle une méthode de l'API si l'utilisateur est lui-même.
|
|
69
|
+
*
|
|
70
|
+
* @param {string|function} param - Le nom de la méthode à appeler ou une fonction de rappel.
|
|
71
|
+
* @param {object} data - Les données à passer à la méthode.
|
|
72
|
+
* @returns {Promise<any>} - La promesse de la méthode appelée.
|
|
73
|
+
* @throws {ApiAuthenticationError} - Si l'utilisateur n'est pas lui-même.
|
|
74
|
+
* @throws {ApiValidationError} - Si une erreur se produit lors de l'appel de l'API.
|
|
75
|
+
* @throws {ApiResponseError} - Si une erreur se produit lors de l'appel de l'API.
|
|
76
|
+
* @throws {ApiClientError} - Si une erreur se produit lors de l'appel de l'API.
|
|
77
|
+
*/
|
|
78
|
+
async callIsMe(param, data = {}) {
|
|
79
|
+
if (!this.isMe) {
|
|
80
|
+
throw new ApiAuthenticationError("Vous devez être vous-même pour faire cette action.");
|
|
81
|
+
}
|
|
82
|
+
// Si le premier paramètre est une fonction, on l'exécute en tant que callback
|
|
83
|
+
if (typeof param === "function") {
|
|
84
|
+
return await param();
|
|
85
|
+
}
|
|
86
|
+
// Sinon, on considère qu'il s'agit d'un constant et on appelle la méthode par défaut
|
|
87
|
+
return await this.callIsConnected(param, data);
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async validateImage(imageInput){
|
|
91
|
+
const image = await this._validateUploadInput(imageInput, {
|
|
92
|
+
allowedMimeTypes: ["image/jpeg", "image/png", "image/jpg"],
|
|
93
|
+
expectedType: "image"
|
|
94
|
+
});
|
|
95
|
+
return image;
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async validateFile(fileInput){
|
|
99
|
+
const file = await this._validateUploadInput(fileInput, {
|
|
100
|
+
allowedMimeTypes: ["application/pdf", "text/plain", "text/csv"],
|
|
101
|
+
expectedType: "file"
|
|
102
|
+
});
|
|
103
|
+
return file;
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async _validateUploadInput(input, { allowedMimeTypes = [], expectedType = "any" }) {
|
|
107
|
+
if (!input) {
|
|
108
|
+
throw new ApiValidationError("Le fichier est requis.");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const isNode = typeof window === "undefined" && typeof process !== "undefined";
|
|
112
|
+
let mimeType = "";
|
|
113
|
+
let output = input;
|
|
114
|
+
|
|
115
|
+
// Navigateur : File
|
|
116
|
+
if (typeof File !== "undefined" && input instanceof File) {
|
|
117
|
+
mimeType = input.type;
|
|
118
|
+
if (!allowedMimeTypes.includes(mimeType)) {
|
|
119
|
+
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Navigateur : Blob
|
|
124
|
+
else if (typeof Blob !== "undefined" && input instanceof Blob) {
|
|
125
|
+
mimeType = input.type;
|
|
126
|
+
if (!allowedMimeTypes.includes(mimeType)) {
|
|
127
|
+
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ext = mimeType.split("/")[1] || "bin";
|
|
131
|
+
const fileName = `${Date.now()}.${ext}`;
|
|
132
|
+
output = new File([input], fileName, { type: mimeType });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Node.js : Buffer
|
|
136
|
+
else if (isNode && Buffer.isBuffer(input)) {
|
|
137
|
+
const fileTypeResult = await fileTypeFromBuffer(input);
|
|
138
|
+
mimeType = fileTypeResult?.mime;
|
|
139
|
+
|
|
140
|
+
if (!fileTypeResult || !allowedMimeTypes.includes(mimeType)) {
|
|
141
|
+
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Pour un fichier image, on transforme en stream
|
|
145
|
+
if (expectedType === "image") {
|
|
146
|
+
const ext = fileTypeResult.ext;
|
|
147
|
+
const filename = `${Date.now()}.${ext}`;
|
|
148
|
+
output = await this._createReadStreamFromBuffer(input, filename, mimeType);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Node.js : ReadableStream
|
|
153
|
+
else if (isNode && input?.readable && typeof input._read === "function") {
|
|
154
|
+
const previewChunks = [];
|
|
155
|
+
const tee = await this._passThrough();
|
|
156
|
+
const resultStream = await this._passThrough();
|
|
157
|
+
|
|
158
|
+
const MAX_BYTES = 4100;
|
|
159
|
+
let bytesRead = 0;
|
|
160
|
+
|
|
161
|
+
input.on("data", (chunk) => {
|
|
162
|
+
if (bytesRead < MAX_BYTES) {
|
|
163
|
+
previewChunks.push(chunk);
|
|
164
|
+
bytesRead += chunk.length;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
input.pipe(tee).pipe(resultStream);
|
|
169
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
170
|
+
|
|
171
|
+
const previewBuffer = Buffer.concat(previewChunks);
|
|
172
|
+
const fileTypeResult = await fileTypeFromBuffer(previewBuffer);
|
|
173
|
+
mimeType = fileTypeResult?.mime;
|
|
174
|
+
|
|
175
|
+
if (!fileTypeResult || !allowedMimeTypes.includes(mimeType)) {
|
|
176
|
+
throw new ApiValidationError("Le type du fichier est invalide.");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
resultStream.path = `${Date.now()}.${fileTypeResult.ext}`;
|
|
180
|
+
resultStream.mimeType = mimeType;
|
|
181
|
+
output = resultStream;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
else {
|
|
185
|
+
throw new ApiValidationError("Type de fichier non reconnu.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return output;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Transforme un Buffer en ReadableStream équivalent à fs.createReadStream
|
|
193
|
+
* @param {Buffer} buffer - Le buffer contenant les données binaires
|
|
194
|
+
* @param {string} filename - Nom de fichier (utilisé dans FormData)
|
|
195
|
+
* @param {string} mimeType - Type MIME (utilisé dans FormData)
|
|
196
|
+
* @returns {Object} - { stream, filename, mimeType }
|
|
197
|
+
*/
|
|
198
|
+
async _createReadStreamFromBuffer(buffer, filename = "file.bin", mimeType = "application/octet-stream") {
|
|
199
|
+
const stream = await this._bufferToReadable(buffer);
|
|
200
|
+
stream.path = filename; // 👈 hack pour simuler un vrai fichier ReadStream
|
|
201
|
+
stream.mimeType = mimeType;
|
|
202
|
+
return stream;
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
async _bufferToReadable(buffer) {
|
|
206
|
+
if (typeof window === "undefined") {
|
|
207
|
+
const { bufferToReadable } = await import("../utils/stream-utils.node.js");
|
|
208
|
+
return bufferToReadable(buffer);
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
async _passThrough() {
|
|
215
|
+
if (typeof window === "undefined") {
|
|
216
|
+
const { createPassThrough } = await import("../utils/stream-utils.node.js");
|
|
217
|
+
return createPassThrough();
|
|
218
|
+
} else {
|
|
219
|
+
throw new Error("passThrough ne doit pas être appelé dans le navigateur");
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
// async _bufferToReadable(buffer) {
|
|
224
|
+
// if (typeof window === "undefined") {
|
|
225
|
+
// const { Readable } = await import("stream");
|
|
226
|
+
// return Readable.from(buffer);
|
|
227
|
+
// } else {
|
|
228
|
+
// throw new Error("bufferToReadable ne doit pas être appelé dans le navigateur");
|
|
229
|
+
// }
|
|
230
|
+
// },
|
|
231
|
+
|
|
232
|
+
// async _passThrough() {
|
|
233
|
+
// if (typeof window === "undefined") {
|
|
234
|
+
// const { PassThrough } = await import("stream");
|
|
235
|
+
// return new PassThrough();
|
|
236
|
+
// } else {
|
|
237
|
+
// throw new Error("passThrough ne doit pas être appelé dans le navigateur");
|
|
238
|
+
// }
|
|
239
|
+
// },
|
|
240
|
+
|
|
241
|
+
_omitProps(obj, propsToRemove) {
|
|
242
|
+
if (!obj || typeof obj !== "object") return {};
|
|
243
|
+
|
|
244
|
+
const result = { ...obj };
|
|
245
|
+
for (const prop of propsToRemove) {
|
|
246
|
+
delete result[prop];
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
_pickProps(obj, keys, transforms = {}) {
|
|
252
|
+
if (!obj || typeof obj !== "object") return {};
|
|
253
|
+
|
|
254
|
+
const result = {};
|
|
255
|
+
|
|
256
|
+
for (const key of keys) {
|
|
257
|
+
if (key in obj) {
|
|
258
|
+
const value = obj[key];
|
|
259
|
+
if (typeof transforms[key] === "function") {
|
|
260
|
+
result[key] = transforms[key](value, obj); // (valeur, objet source)
|
|
261
|
+
} else {
|
|
262
|
+
result[key] = value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return result;
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
_createFilteredProxy(list) {
|
|
271
|
+
return new Proxy(list, {
|
|
272
|
+
get(target, prop, receiver) {
|
|
273
|
+
if (typeof prop === "string" && !isNaN(prop)) {
|
|
274
|
+
const active = target.filter(n => !n._isDeleted);
|
|
275
|
+
return active[prop];
|
|
276
|
+
}
|
|
277
|
+
if (prop === "length") {
|
|
278
|
+
return target.filter(n => !n._isDeleted).length;
|
|
279
|
+
}
|
|
280
|
+
if (typeof target[prop] === "function") {
|
|
281
|
+
return (...args) => target.filter(n => !n._isDeleted)[prop](...args);
|
|
282
|
+
}
|
|
283
|
+
return Reflect.get(target, prop, receiver);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
_newId() {
|
|
289
|
+
const newId = new ObjectID();
|
|
290
|
+
return newId.toString();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
};
|
|
294
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// utils/stream-utils.node.js
|
|
2
|
+
export async function bufferToReadable(buffer) {
|
|
3
|
+
const { Readable } = await import("stream");
|
|
4
|
+
return Readable.from(buffer);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function createPassThrough() {
|
|
8
|
+
const { PassThrough } = await import("stream");
|
|
9
|
+
return new PassThrough();
|
|
10
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
|