@frenchbaas/js 0.1.0
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/README.md +98 -0
- package/dist/index.cjs +541 -0
- package/dist/index.d.mts +355 -0
- package/dist/index.d.ts +355 -0
- package/dist/index.js +506 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @frenchbaas/js
|
|
2
|
+
|
|
3
|
+
SDK JavaScript officiel pour [FrenchBaas](https://frenchbaas.fr) — le BaaS français.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @frenchbaas/js
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Démarrage rapide
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { FrenchBaas } from '@frenchbaas/js'
|
|
15
|
+
|
|
16
|
+
const client = new FrenchBaas({
|
|
17
|
+
url: 'https://api.frenchbaas.fr',
|
|
18
|
+
apiKey: 'votre-clé-api',
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Auth
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// Inscription
|
|
26
|
+
const { user } = await client.auth.signUp({ email: 'a@b.com', password: 'secret' })
|
|
27
|
+
|
|
28
|
+
// Connexion
|
|
29
|
+
const { user } = await client.auth.login({ email: 'a@b.com', password: 'secret' })
|
|
30
|
+
|
|
31
|
+
// Déconnexion
|
|
32
|
+
await client.auth.logout()
|
|
33
|
+
|
|
34
|
+
// État
|
|
35
|
+
client.auth.getUser() // { id, email } | null
|
|
36
|
+
client.auth.isLoggedIn() // boolean
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Collections (documents)
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const col = client.collection('collection-uuid')
|
|
43
|
+
|
|
44
|
+
// Lire (paginé)
|
|
45
|
+
const { data, meta } = await col.get({ page: 1, perPage: 20 })
|
|
46
|
+
|
|
47
|
+
// Créer
|
|
48
|
+
const doc = await col.create({ title: 'Hello', published: true })
|
|
49
|
+
|
|
50
|
+
// Mettre à jour
|
|
51
|
+
const doc = await col.update('doc-id', { title: 'Modifié' })
|
|
52
|
+
|
|
53
|
+
// Supprimer
|
|
54
|
+
await col.delete('doc-id')
|
|
55
|
+
|
|
56
|
+
// Schéma de la collection
|
|
57
|
+
const schema = await col.schema()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Gestion des erreurs
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { AuthError, ValidationError, NotFoundError, QuotaError, NetworkError } from '@frenchbaas/js'
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await client.collection('col-id').create({ title: 'Test' })
|
|
67
|
+
} catch (e) {
|
|
68
|
+
if (e instanceof AuthError) console.error('Non connecté')
|
|
69
|
+
if (e instanceof ValidationError) console.error('Données invalides', e.errors)
|
|
70
|
+
if (e instanceof NotFoundError) console.error('Introuvable')
|
|
71
|
+
if (e instanceof QuotaError) console.error('Quota dépassé')
|
|
72
|
+
if (e instanceof NetworkError) console.error('Hors ligne')
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Options
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
// Persister la session dans localStorage (rechargement de page)
|
|
80
|
+
const client = new FrenchBaas({
|
|
81
|
+
url: 'https://api.frenchbaas.fr',
|
|
82
|
+
apiKey: 'votre-clé-api',
|
|
83
|
+
storage: 'localStorage', // défaut : 'memory'
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Fonctionnalités automatiques
|
|
88
|
+
|
|
89
|
+
- **Refresh token transparent** — access token rafraîchi automatiquement avant expiration
|
|
90
|
+
- **Retry sur 401** — relance la requête après refresh sans intervention du développeur
|
|
91
|
+
- **Déduplication** — plusieurs requêtes simultanées ne déclenchent qu'un seul refresh
|
|
92
|
+
- **Erreurs typées** — chaque erreur HTTP a sa classe TypeScript dédiée
|
|
93
|
+
- **Zero dépendance** — aucune dépendance runtime
|
|
94
|
+
- **TypeScript** — types inclus
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AuthError: () => AuthError,
|
|
24
|
+
FrenchBaas: () => FrenchBaasClient,
|
|
25
|
+
FrenchBaasError: () => FrenchBaasError,
|
|
26
|
+
NetworkError: () => NetworkError,
|
|
27
|
+
NotFoundError: () => NotFoundError,
|
|
28
|
+
QuotaError: () => QuotaError,
|
|
29
|
+
RateLimitError: () => RateLimitError,
|
|
30
|
+
ServerError: () => ServerError,
|
|
31
|
+
ValidationError: () => ValidationError
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/errors.ts
|
|
36
|
+
var FrenchBaasError = class extends Error {
|
|
37
|
+
constructor(message, status) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.status = status;
|
|
40
|
+
this.name = "FrenchBaasError";
|
|
41
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var AuthError = class extends FrenchBaasError {
|
|
45
|
+
constructor(message, status) {
|
|
46
|
+
super(message, status);
|
|
47
|
+
this.name = "AuthError";
|
|
48
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var NetworkError = class extends FrenchBaasError {
|
|
52
|
+
constructor(message = "Impossible de joindre le serveur FrenchBaas.") {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "NetworkError";
|
|
55
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var ValidationError = class extends FrenchBaasError {
|
|
59
|
+
constructor(message, errors = []) {
|
|
60
|
+
super(message, 422);
|
|
61
|
+
this.errors = errors;
|
|
62
|
+
this.name = "ValidationError";
|
|
63
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var NotFoundError = class extends FrenchBaasError {
|
|
67
|
+
constructor(message = "Ressource introuvable.") {
|
|
68
|
+
super(message, 404);
|
|
69
|
+
this.name = "NotFoundError";
|
|
70
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var QuotaError = class extends FrenchBaasError {
|
|
74
|
+
constructor(message) {
|
|
75
|
+
super(message, 403);
|
|
76
|
+
this.name = "QuotaError";
|
|
77
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var RateLimitError = class extends FrenchBaasError {
|
|
81
|
+
constructor(message = "Trop de requ\xEAtes. Veuillez r\xE9essayer dans quelques instants.") {
|
|
82
|
+
super(message, 429);
|
|
83
|
+
this.name = "RateLimitError";
|
|
84
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var ServerError = class extends FrenchBaasError {
|
|
88
|
+
constructor(message = "Erreur interne du serveur.", status) {
|
|
89
|
+
super(message, status);
|
|
90
|
+
this.name = "ServerError";
|
|
91
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// src/auth.ts
|
|
96
|
+
var AuthModule = class {
|
|
97
|
+
constructor(_http, _tokenStore, _baseUrl, _apiKey) {
|
|
98
|
+
this._http = _http;
|
|
99
|
+
this._tokenStore = _tokenStore;
|
|
100
|
+
this._baseUrl = _baseUrl;
|
|
101
|
+
this._apiKey = _apiKey;
|
|
102
|
+
this._http.setRefreshFn(() => this._refresh());
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Inscrit un nouvel utilisateur final.
|
|
106
|
+
* Stocke automatiquement les tokens après inscription réussie.
|
|
107
|
+
*/
|
|
108
|
+
async signUp(params) {
|
|
109
|
+
const res = await this._http.post("/sdk/auth/signup", params);
|
|
110
|
+
return this._storeAndReturn(res);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Connecte un utilisateur existant.
|
|
114
|
+
* Stocke automatiquement les tokens après connexion réussie.
|
|
115
|
+
*/
|
|
116
|
+
async login(params) {
|
|
117
|
+
const res = await this._http.post("/sdk/auth/login", params);
|
|
118
|
+
return this._storeAndReturn(res);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Déconnecte l'utilisateur et efface tous les tokens stockés.
|
|
122
|
+
* L'appel API est best-effort (stateless côté serveur).
|
|
123
|
+
*/
|
|
124
|
+
async logout() {
|
|
125
|
+
try {
|
|
126
|
+
await this._http.post("/sdk/auth/logout");
|
|
127
|
+
} finally {
|
|
128
|
+
this._tokenStore.clear();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Retourne l'utilisateur courant depuis la mémoire (synchrone).
|
|
133
|
+
* Retourne null si non connecté ou si la page a été rechargée
|
|
134
|
+
* avec la stratégie 'memory'.
|
|
135
|
+
*/
|
|
136
|
+
getUser() {
|
|
137
|
+
return this._tokenStore.user;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Indique si l'utilisateur a une session active (refresh token valide).
|
|
141
|
+
*/
|
|
142
|
+
isLoggedIn() {
|
|
143
|
+
return this._tokenStore.refreshToken !== null && !this._tokenStore.isRefreshTokenExpired();
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Refresh interne — appelé automatiquement par HttpClient.
|
|
147
|
+
* Utilise un fetch direct pour éviter la récursion infinie.
|
|
148
|
+
* Le refresh token expiré → logout propre + AuthError.
|
|
149
|
+
*/
|
|
150
|
+
async _refresh() {
|
|
151
|
+
const refreshToken = this._tokenStore.refreshToken;
|
|
152
|
+
if (!refreshToken || this._tokenStore.isRefreshTokenExpired()) {
|
|
153
|
+
this._tokenStore.clear();
|
|
154
|
+
throw new AuthError("Session expir\xE9e. Veuillez vous reconnecter.", 401);
|
|
155
|
+
}
|
|
156
|
+
let response;
|
|
157
|
+
try {
|
|
158
|
+
response = await fetch(`${this._baseUrl}/sdk/auth/refresh`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: {
|
|
161
|
+
"Content-Type": "application/json",
|
|
162
|
+
"Accept": "application/json",
|
|
163
|
+
"X-Api-Key": this._apiKey
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify({ refresh_token: refreshToken })
|
|
166
|
+
});
|
|
167
|
+
} catch {
|
|
168
|
+
throw new NetworkError();
|
|
169
|
+
}
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
this._tokenStore.clear();
|
|
172
|
+
let msg = "Session expir\xE9e. Veuillez vous reconnecter.";
|
|
173
|
+
try {
|
|
174
|
+
const body2 = await response.json();
|
|
175
|
+
if (body2.error) msg = body2.error;
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
throw new AuthError(msg, 401);
|
|
179
|
+
}
|
|
180
|
+
const body = await response.json();
|
|
181
|
+
this._tokenStore.updateTokens(
|
|
182
|
+
body.data.access_token,
|
|
183
|
+
body.data.refresh_token
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
_storeAndReturn(res) {
|
|
187
|
+
const { access_token, refresh_token, user } = res.data;
|
|
188
|
+
if (!user) throw new AuthError("R\xE9ponse serveur invalide : utilisateur manquant.");
|
|
189
|
+
this._tokenStore.set({ accessToken: access_token, refreshToken: refresh_token, user });
|
|
190
|
+
return { user, access_token, refresh_token };
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/collection.ts
|
|
195
|
+
var CollectionClient = class {
|
|
196
|
+
constructor(_collectionId, _http) {
|
|
197
|
+
this._collectionId = _collectionId;
|
|
198
|
+
this._http = _http;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Liste les documents de la collection avec pagination.
|
|
202
|
+
* La visibilité (public/authenticated/private) est gérée côté serveur.
|
|
203
|
+
*
|
|
204
|
+
* @param options.page Numéro de page (défaut: 1)
|
|
205
|
+
* @param options.perPage Documents par page, max 100 (défaut: 50)
|
|
206
|
+
*/
|
|
207
|
+
async get(options = {}) {
|
|
208
|
+
const params = {
|
|
209
|
+
page: options.page,
|
|
210
|
+
per_page: options.perPage
|
|
211
|
+
};
|
|
212
|
+
const res = await this._http.get(
|
|
213
|
+
`/sdk/collections/${this._collectionId}/documents`,
|
|
214
|
+
params
|
|
215
|
+
);
|
|
216
|
+
return { data: res.data, meta: res.meta };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Crée un nouveau document dans la collection.
|
|
220
|
+
* Les champs sont validés contre le schéma côté serveur.
|
|
221
|
+
*
|
|
222
|
+
* @param data Données du document (doit respecter le schéma de la collection)
|
|
223
|
+
*/
|
|
224
|
+
async create(data) {
|
|
225
|
+
const res = await this._http.post(
|
|
226
|
+
`/sdk/collections/${this._collectionId}/documents`,
|
|
227
|
+
{ data }
|
|
228
|
+
);
|
|
229
|
+
return res.data;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Met à jour partiellement un document (merge).
|
|
233
|
+
* Seuls les champs fournis sont mis à jour, les autres restent inchangés.
|
|
234
|
+
*
|
|
235
|
+
* - Collection private : seul le créateur peut modifier.
|
|
236
|
+
* - Collection authenticated/public : tout utilisateur authentifié peut modifier.
|
|
237
|
+
*
|
|
238
|
+
* @param documentId UUID du document à modifier
|
|
239
|
+
* @param data Champs à mettre à jour
|
|
240
|
+
*/
|
|
241
|
+
async update(documentId, data) {
|
|
242
|
+
const res = await this._http.patch(
|
|
243
|
+
`/sdk/documents/${documentId}`,
|
|
244
|
+
{ data }
|
|
245
|
+
);
|
|
246
|
+
return res.data;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Supprime un document.
|
|
250
|
+
*
|
|
251
|
+
* - Collection private : seul le créateur peut supprimer.
|
|
252
|
+
* - Collection authenticated/public : tout utilisateur authentifié peut supprimer.
|
|
253
|
+
*
|
|
254
|
+
* @param documentId UUID du document à supprimer
|
|
255
|
+
*/
|
|
256
|
+
async delete(documentId) {
|
|
257
|
+
await this._http.delete(`/sdk/documents/${documentId}`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Retourne le schéma de la collection (nom des champs, types, visibilité).
|
|
261
|
+
* Utile pour construire des formulaires dynamiques.
|
|
262
|
+
*/
|
|
263
|
+
async schema() {
|
|
264
|
+
const res = await this._http.get(
|
|
265
|
+
`/sdk/collections/${this._collectionId}/schema`
|
|
266
|
+
);
|
|
267
|
+
return res.data;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/http.ts
|
|
272
|
+
var HttpClient = class {
|
|
273
|
+
constructor(_baseUrl, _apiKey, _tokenStore) {
|
|
274
|
+
this._baseUrl = _baseUrl;
|
|
275
|
+
this._apiKey = _apiKey;
|
|
276
|
+
this._tokenStore = _tokenStore;
|
|
277
|
+
this._refreshFn = null;
|
|
278
|
+
this._refreshing = null;
|
|
279
|
+
}
|
|
280
|
+
/** Enregistre la fonction de refresh (injectée par AuthModule). */
|
|
281
|
+
setRefreshFn(fn) {
|
|
282
|
+
this._refreshFn = fn;
|
|
283
|
+
}
|
|
284
|
+
async get(path, params) {
|
|
285
|
+
const url = new URL(this._baseUrl + path);
|
|
286
|
+
if (params) {
|
|
287
|
+
for (const [k, v] of Object.entries(params)) {
|
|
288
|
+
if (v !== void 0) url.searchParams.set(k, String(v));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return this._request("GET", url.toString(), void 0, true);
|
|
292
|
+
}
|
|
293
|
+
async post(path, body) {
|
|
294
|
+
return this._request("POST", this._baseUrl + path, body, true);
|
|
295
|
+
}
|
|
296
|
+
async patch(path, body) {
|
|
297
|
+
return this._request("PATCH", this._baseUrl + path, body, true);
|
|
298
|
+
}
|
|
299
|
+
async delete(path) {
|
|
300
|
+
return this._request("DELETE", this._baseUrl + path, void 0, true);
|
|
301
|
+
}
|
|
302
|
+
async _request(method, url, body, canRetry) {
|
|
303
|
+
if (canRetry && this._tokenStore.accessToken && this._tokenStore.isAccessTokenExpired()) {
|
|
304
|
+
await this._triggerRefresh();
|
|
305
|
+
}
|
|
306
|
+
const response = await this._fetch(method, url, body);
|
|
307
|
+
if (response.status === 401 && canRetry && this._refreshFn && !this._tokenStore.isRefreshTokenExpired()) {
|
|
308
|
+
try {
|
|
309
|
+
await this._triggerRefresh();
|
|
310
|
+
return this._request(method, url, body, false);
|
|
311
|
+
} catch {
|
|
312
|
+
throw new AuthError("Session expir\xE9e. Veuillez vous reconnecter.", 401);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return this._parseResponse(response);
|
|
316
|
+
}
|
|
317
|
+
async _fetch(method, url, body) {
|
|
318
|
+
const headers = {
|
|
319
|
+
"Content-Type": "application/json",
|
|
320
|
+
"Accept": "application/json",
|
|
321
|
+
"X-Api-Key": this._apiKey
|
|
322
|
+
};
|
|
323
|
+
const token = this._tokenStore.accessToken;
|
|
324
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
325
|
+
try {
|
|
326
|
+
return await fetch(url, {
|
|
327
|
+
method,
|
|
328
|
+
headers,
|
|
329
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
330
|
+
});
|
|
331
|
+
} catch {
|
|
332
|
+
throw new NetworkError();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Déclenche le refresh en s'assurant qu'un seul refresh tourne à la fois
|
|
337
|
+
* même si plusieurs requêtes l'appellent simultanément.
|
|
338
|
+
*/
|
|
339
|
+
_triggerRefresh() {
|
|
340
|
+
if (!this._refreshFn) return Promise.resolve();
|
|
341
|
+
if (this._refreshing) return this._refreshing;
|
|
342
|
+
this._refreshing = this._refreshFn().finally(() => {
|
|
343
|
+
this._refreshing = null;
|
|
344
|
+
});
|
|
345
|
+
return this._refreshing;
|
|
346
|
+
}
|
|
347
|
+
async _parseResponse(response) {
|
|
348
|
+
let body;
|
|
349
|
+
try {
|
|
350
|
+
body = await response.json();
|
|
351
|
+
} catch {
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
throw new ServerError(`Erreur serveur (${response.status})`, response.status);
|
|
354
|
+
}
|
|
355
|
+
throw new ServerError("R\xE9ponse invalide du serveur.");
|
|
356
|
+
}
|
|
357
|
+
if (response.ok) return body;
|
|
358
|
+
const err = body;
|
|
359
|
+
const msg = err.error ?? "Une erreur est survenue.";
|
|
360
|
+
switch (response.status) {
|
|
361
|
+
case 400:
|
|
362
|
+
throw new ValidationError(msg);
|
|
363
|
+
case 401:
|
|
364
|
+
throw new AuthError(msg, 401);
|
|
365
|
+
case 403:
|
|
366
|
+
if (err.quota || /quota|limit|dépass/i.test(msg)) {
|
|
367
|
+
throw new QuotaError(msg);
|
|
368
|
+
}
|
|
369
|
+
throw new AuthError(msg, 403);
|
|
370
|
+
case 404:
|
|
371
|
+
throw new NotFoundError(msg);
|
|
372
|
+
case 413:
|
|
373
|
+
throw new ValidationError(msg);
|
|
374
|
+
case 422: {
|
|
375
|
+
const details = err.errors ?? [];
|
|
376
|
+
throw new ValidationError(details.length ? details.join(", ") : msg, details);
|
|
377
|
+
}
|
|
378
|
+
case 429:
|
|
379
|
+
throw new RateLimitError();
|
|
380
|
+
default:
|
|
381
|
+
if (response.status >= 500) throw new ServerError(msg, response.status);
|
|
382
|
+
throw new ServerError(msg, response.status);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/token.ts
|
|
388
|
+
var KEYS = {
|
|
389
|
+
access: "frenchbaas_access_token",
|
|
390
|
+
refresh: "frenchbaas_refresh_token",
|
|
391
|
+
user: "frenchbaas_user"
|
|
392
|
+
};
|
|
393
|
+
var TokenStore = class {
|
|
394
|
+
constructor(strategy = "memory") {
|
|
395
|
+
this._state = { accessToken: null, refreshToken: null, user: null };
|
|
396
|
+
this._strategy = strategy;
|
|
397
|
+
if (strategy === "localStorage") {
|
|
398
|
+
this._loadFromStorage();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
get accessToken() {
|
|
402
|
+
return this._state.accessToken;
|
|
403
|
+
}
|
|
404
|
+
get refreshToken() {
|
|
405
|
+
return this._state.refreshToken;
|
|
406
|
+
}
|
|
407
|
+
get user() {
|
|
408
|
+
return this._state.user;
|
|
409
|
+
}
|
|
410
|
+
/** Stocke access token, refresh token et optionnellement l'utilisateur. */
|
|
411
|
+
set(tokens) {
|
|
412
|
+
this._state.accessToken = tokens.accessToken;
|
|
413
|
+
this._state.refreshToken = tokens.refreshToken;
|
|
414
|
+
if (tokens.user !== void 0) this._state.user = tokens.user;
|
|
415
|
+
if (this._strategy === "localStorage") this._persist();
|
|
416
|
+
}
|
|
417
|
+
/** Met à jour uniquement les tokens (appelé après un refresh — sans user). */
|
|
418
|
+
updateTokens(accessToken, refreshToken) {
|
|
419
|
+
this._state.accessToken = accessToken;
|
|
420
|
+
this._state.refreshToken = refreshToken;
|
|
421
|
+
if (this._strategy === "localStorage") this._persist();
|
|
422
|
+
}
|
|
423
|
+
/** Efface tous les tokens et l'utilisateur (appelé lors du logout). */
|
|
424
|
+
clear() {
|
|
425
|
+
this._state = { accessToken: null, refreshToken: null, user: null };
|
|
426
|
+
if (this._strategy === "localStorage") {
|
|
427
|
+
try {
|
|
428
|
+
localStorage.removeItem(KEYS.access);
|
|
429
|
+
localStorage.removeItem(KEYS.refresh);
|
|
430
|
+
localStorage.removeItem(KEYS.user);
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Vérifie si l'access token est expiré ou expire dans moins de 30 secondes.
|
|
437
|
+
* Retourne true si absent ou invalide.
|
|
438
|
+
*/
|
|
439
|
+
isAccessTokenExpired() {
|
|
440
|
+
return this._isExpired(this._state.accessToken, 3e4);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Vérifie si le refresh token est expiré.
|
|
444
|
+
* Retourne true si absent ou invalide.
|
|
445
|
+
*/
|
|
446
|
+
isRefreshTokenExpired() {
|
|
447
|
+
return this._isExpired(this._state.refreshToken, 0);
|
|
448
|
+
}
|
|
449
|
+
_isExpired(token, bufferMs) {
|
|
450
|
+
if (!token) return true;
|
|
451
|
+
try {
|
|
452
|
+
const payload = decodeJwtPayload(token);
|
|
453
|
+
return payload.exp * 1e3 < Date.now() + bufferMs;
|
|
454
|
+
} catch {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
_persist() {
|
|
459
|
+
try {
|
|
460
|
+
if (this._state.accessToken) localStorage.setItem(KEYS.access, this._state.accessToken);
|
|
461
|
+
if (this._state.refreshToken) localStorage.setItem(KEYS.refresh, this._state.refreshToken);
|
|
462
|
+
if (this._state.user) localStorage.setItem(KEYS.user, JSON.stringify(this._state.user));
|
|
463
|
+
} catch {
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
_loadFromStorage() {
|
|
467
|
+
try {
|
|
468
|
+
this._state.accessToken = localStorage.getItem(KEYS.access);
|
|
469
|
+
this._state.refreshToken = localStorage.getItem(KEYS.refresh);
|
|
470
|
+
const raw = localStorage.getItem(KEYS.user);
|
|
471
|
+
this._state.user = raw ? JSON.parse(raw) : null;
|
|
472
|
+
} catch {
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
function decodeJwtPayload(token) {
|
|
477
|
+
const parts = token.split(".");
|
|
478
|
+
if (parts.length !== 3) throw new Error("JWT invalide : format incorrect");
|
|
479
|
+
const base64 = (parts[1] ?? "").replace(/-/g, "+").replace(/_/g, "/");
|
|
480
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
481
|
+
let json;
|
|
482
|
+
if (typeof atob !== "undefined") {
|
|
483
|
+
json = atob(padded);
|
|
484
|
+
} else {
|
|
485
|
+
json = globalThis.Buffer.from(padded, "base64").toString("utf-8");
|
|
486
|
+
}
|
|
487
|
+
const payload = JSON.parse(json);
|
|
488
|
+
if (typeof payload.exp !== "number") throw new Error("JWT invalide : champ exp manquant");
|
|
489
|
+
return payload;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/client.ts
|
|
493
|
+
var FrenchBaasClient = class {
|
|
494
|
+
constructor(config) {
|
|
495
|
+
if (!config.url) throw new Error('[FrenchBaas] La propri\xE9t\xE9 "url" est requise.');
|
|
496
|
+
if (!config.apiKey) throw new Error('[FrenchBaas] La propri\xE9t\xE9 "apiKey" est requise.');
|
|
497
|
+
const baseUrl = config.url.replace(/\/$/, "");
|
|
498
|
+
this._tokenStore = new TokenStore(config.storage ?? "memory");
|
|
499
|
+
this._http = new HttpClient(baseUrl, config.apiKey, this._tokenStore);
|
|
500
|
+
this.auth = new AuthModule(this._http, this._tokenStore, baseUrl, config.apiKey);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Retourne un client pour une collection spécifique.
|
|
504
|
+
*
|
|
505
|
+
* Le type générique T permet de typer les données des documents :
|
|
506
|
+
* ```ts
|
|
507
|
+
* const posts = client.collection<{ title: string; published: boolean }>('uuid')
|
|
508
|
+
* const doc = await posts.create({ title: 'Hello', published: true })
|
|
509
|
+
* // doc.data.title est typé string ✓
|
|
510
|
+
* ```
|
|
511
|
+
*
|
|
512
|
+
* @param collectionId UUID de la collection (visible dans le Dashboard)
|
|
513
|
+
*/
|
|
514
|
+
collection(collectionId) {
|
|
515
|
+
if (!collectionId || typeof collectionId !== "string") {
|
|
516
|
+
throw new Error("[FrenchBaas] collection() requiert un UUID de collection valide.");
|
|
517
|
+
}
|
|
518
|
+
return new CollectionClient(collectionId, this._http);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Retourne la spécification OpenAPI du projet.
|
|
522
|
+
* Contient toutes les collections, leurs schémas et les endpoints disponibles.
|
|
523
|
+
* Utile pour de la génération de code ou de la documentation dynamique.
|
|
524
|
+
*/
|
|
525
|
+
async getOpenApiSpec() {
|
|
526
|
+
return this._http.get("/sdk/openapi");
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
530
|
+
0 && (module.exports = {
|
|
531
|
+
AuthError,
|
|
532
|
+
FrenchBaas,
|
|
533
|
+
FrenchBaasError,
|
|
534
|
+
NetworkError,
|
|
535
|
+
NotFoundError,
|
|
536
|
+
QuotaError,
|
|
537
|
+
RateLimitError,
|
|
538
|
+
ServerError,
|
|
539
|
+
ValidationError
|
|
540
|
+
});
|
|
541
|
+
//# sourceMappingURL=index.cjs.map
|