@frenchbaas/js 0.3.0 → 0.4.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 CHANGED
@@ -24,11 +24,11 @@ const client = new FrenchBaas({
24
24
  ## Auth
25
25
 
26
26
  ```js
27
- // Inscription
28
- const { user, access_token } = await client.auth.signUp({ email: 'a@b.com', password: 'secret' })
27
+ // Inscription (mot de passe : 8 caractères minimum)
28
+ const { user, access_token } = await client.auth.signUp({ email: 'a@b.com', password: 'monMotDePasse' })
29
29
 
30
30
  // Connexion
31
- const { user, access_token } = await client.auth.login({ email: 'a@b.com', password: 'secret' })
31
+ const { user, access_token } = await client.auth.login({ email: 'a@b.com', password: 'monMotDePasse' })
32
32
 
33
33
  // Déconnexion
34
34
  await client.auth.logout()
@@ -49,12 +49,23 @@ const col = client.collection('uuid-de-la-collection')
49
49
  const { data, meta } = await col.get({ page: 1, perPage: 20 })
50
50
  // meta → { total, page, per_page, total_pages }
51
51
 
52
- // Filtrer par champ (string, number, boolean)
52
+ // Filtrer par champ valeur unique
53
53
  const { data: pending } = await col.get({ filter: { status: 'pending' } })
54
54
 
55
- // Filtrer par champ references (tableau d'UUIDs contient l'UUID)
55
+ // Filtrer OR plusieurs valeurs (status pending OU delivered)
56
+ const { data: multi } = await col.get({ filter: { status: ['pending', 'delivered'] } })
57
+
58
+ // Filtrer par references — valeur unique (contient l'UUID)
56
59
  const { data: products } = await col.get({ filter: { category_ids: 'uuid-categorie' } })
57
60
 
61
+ // Filtrer references OR — catégorie A OU B
62
+ const { data: orProducts } = await col.get({ filter: { category_ids: ['uuid-cat-a', 'uuid-cat-b'] } })
63
+
64
+ // Filtrer references AND — dans catégorie A ET B (uniquement pour references/array)
65
+ const { data: andProducts } = await col.get({ filter: { category_ids: { and: ['uuid-cat-a', 'uuid-cat-b'] } } })
66
+
67
+ // ⚠ Maximum 20 valeurs par filtre (OR ou AND)
68
+
58
69
  // Récupérer un document par ID
59
70
  const doc = await col.getById('doc-uuid')
60
71
  // doc → { id, data, created_at, updated_at }
@@ -108,6 +119,61 @@ Ces permissions se configurent dans le Dashboard → collection → schéma →
108
119
 
109
120
  ---
110
121
 
122
+ ## Storage
123
+
124
+ ```js
125
+ // Lister les buckets du projet (X-Api-Key uniquement, pas de Bearer)
126
+ const buckets = await client.storage.listBuckets()
127
+ // → [{ id, name, visibility, public, created_at }, ...]
128
+
129
+ // Obtenir un client bucket
130
+ const bucket = client.storage.bucket('uuid-bucket')
131
+
132
+ // Lister les fichiers (paginé, filtre préfixe optionnel)
133
+ const { data, meta } = await bucket.listObjects({ page: 1, perPage: 20, prefix: 'users/123/' })
134
+ // → data: [{ id, name, content_type, size, created_by, created_at, url? }, ...]
135
+ // → meta: { total, page, per_page, pages }
136
+
137
+ // Upload un fichier (3 étapes gérées automatiquement : présignature → PUT OVH → confirmation)
138
+ // Bearer requis.
139
+ const file = document.querySelector('input[type=file]').files[0]
140
+ const obj = await bucket.upload(file, 'photo.jpg', {
141
+ onProgress: pct => console.log(`${pct}%`), // progression optionnelle (navigateur uniquement)
142
+ })
143
+ // → { id, name, content_type, size, created_by, created_at, url? }
144
+
145
+ // Récupérer l'URL d'un fichier
146
+ const { url, expires_in } = await bucket.getUrl(obj.id)
147
+ // Bucket public → url permanente, expires_in absent
148
+ // Bucket privé → url signée (TTL 3600 s), créateur uniquement
149
+
150
+ // Supprimer un fichier (Bearer requis, créateur uniquement)
151
+ await bucket.delete(obj.id)
152
+ ```
153
+
154
+ ### Visibilité des buckets
155
+
156
+ | Visibilité | listObjects | upload / delete | getUrl |
157
+ |------------|-------------|-----------------|--------|
158
+ | `public` | Sans Bearer | Bearer requis | Sans Bearer (URL permanente) |
159
+ | `private` | Bearer requis (ses fichiers) | Bearer requis | Bearer requis (URL signée 1h) |
160
+
161
+ ### Erreurs Storage
162
+
163
+ ```ts
164
+ import { ConflictError, ValidationError, QuotaError } from '@frenchbaas/js'
165
+
166
+ try {
167
+ await bucket.upload(file, 'photo.jpg')
168
+ } catch (e) {
169
+ if (e instanceof ConflictError) console.error('Nom déjà pris par un autre utilisateur')
170
+ if (e instanceof ValidationError) console.error('Fichier trop grand ou type non autorisé')
171
+ if (e instanceof QuotaError) console.error('Quota storage dépassé')
172
+ }
173
+ ```
174
+
175
+ ---
176
+
111
177
  ## Webhooks
112
178
 
113
179
  Si une collection a un `before_create_url` ou `before_update_url` configuré,
@@ -151,6 +217,7 @@ try {
151
217
  import {
152
218
  FrenchBaasError,
153
219
  AuthError,
220
+ ConflictError,
154
221
  ValidationError,
155
222
  NotFoundError,
156
223
  QuotaError,
@@ -169,6 +236,7 @@ try {
169
236
  console.error(e.message, e.errors)
170
237
  }
171
238
  else if (e instanceof AuthError) console.error('Non connecté ou session expirée')
239
+ else if (e instanceof ConflictError) console.error('Conflit : ressource déjà existante')
172
240
  else if (e instanceof NotFoundError) console.error('Document ou collection introuvable')
173
241
  else if (e instanceof QuotaError) console.error('Quota dépassé :', e.message)
174
242
  else if (e instanceof RateLimitError) console.error('Trop de requêtes, réessayez dans quelques instants')
@@ -206,6 +274,10 @@ const client = new FrenchBaas({
206
274
  Utilisez `localStorage` pour que la session survive au rechargement de page
207
275
  (applications web). Utilisez `memory` pour les environnements serveur (Node.js).
208
276
 
277
+ > **Sécurité** : l'option `localStorage` expose les tokens à toute attaque XSS sur votre page.
278
+ > Assurez-vous que votre application ne charge pas de scripts tiers non maîtrisés
279
+ > et qu'elle applique une politique CSP stricte si vous utilisez ce mode.
280
+
209
281
  ---
210
282
 
211
283
  ## Fonctionnalités automatiques
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthError: () => AuthError,
24
+ ConflictError: () => ConflictError,
24
25
  FrenchBaas: () => FrenchBaasClient,
25
26
  FrenchBaasError: () => FrenchBaasError,
26
27
  NetworkError: () => NetworkError,
@@ -91,6 +92,13 @@ var ServerError = class extends FrenchBaasError {
91
92
  Object.setPrototypeOf(this, new.target.prototype);
92
93
  }
93
94
  };
95
+ var ConflictError = class extends FrenchBaasError {
96
+ constructor(message = "Conflit : la ressource existe d\xE9j\xE0.") {
97
+ super(message, 409);
98
+ this.name = "ConflictError";
99
+ Object.setPrototypeOf(this, new.target.prototype);
100
+ }
101
+ };
94
102
 
95
103
  // src/auth.ts
96
104
  var AuthModule = class {
@@ -211,7 +219,14 @@ var CollectionClient = class {
211
219
  };
212
220
  if (options.filter) {
213
221
  for (const [key, val] of Object.entries(options.filter)) {
214
- if (val !== void 0) params[`filter[${key}]`] = val;
222
+ if (val === void 0) continue;
223
+ if (typeof val === "string") {
224
+ params[`filter[${key}]`] = val;
225
+ } else if (Array.isArray(val)) {
226
+ params[`filter[${key}][]`] = val;
227
+ } else {
228
+ params[`filter[${key}][and][]`] = val.and;
229
+ }
215
230
  }
216
231
  }
217
232
  const res = await this._http.get(
@@ -314,7 +329,12 @@ var HttpClient = class {
314
329
  const url = new URL(this._baseUrl + path);
315
330
  if (params) {
316
331
  for (const [k, v] of Object.entries(params)) {
317
- if (v !== void 0) url.searchParams.set(k, String(v));
332
+ if (v === void 0) continue;
333
+ if (Array.isArray(v)) {
334
+ for (const item of v) url.searchParams.append(k, item);
335
+ } else {
336
+ url.searchParams.set(k, String(v));
337
+ }
318
338
  }
319
339
  }
320
340
  return this._request("GET", url.toString(), void 0, true);
@@ -404,6 +424,8 @@ var HttpClient = class {
404
424
  const details = err.errors ?? [];
405
425
  throw new ValidationError(details.length ? details.join(", ") : msg, details);
406
426
  }
427
+ case 409:
428
+ throw new ConflictError(msg);
407
429
  case 429:
408
430
  throw new RateLimitError();
409
431
  default:
@@ -413,6 +435,139 @@ var HttpClient = class {
413
435
  }
414
436
  };
415
437
 
438
+ // src/storage.ts
439
+ var StorageModule = class {
440
+ constructor(_http) {
441
+ this._http = _http;
442
+ }
443
+ /**
444
+ * Liste tous les buckets du projet.
445
+ *
446
+ * Authentification par API key uniquement — pas de Bearer requis.
447
+ * Usage principal : découverte des IDs pendant la construction de l'app.
448
+ */
449
+ async listBuckets() {
450
+ const res = await this._http.get(
451
+ "/sdk/storage/buckets"
452
+ );
453
+ return res.data;
454
+ }
455
+ /**
456
+ * Retourne un client pour opérer sur un bucket spécifique.
457
+ *
458
+ * @param bucketId UUID du bucket (visible dans Dashboard → Storage)
459
+ */
460
+ bucket(bucketId) {
461
+ if (!bucketId || typeof bucketId !== "string") {
462
+ throw new Error("[FrenchBaas] storage.bucket() requiert un UUID de bucket valide.");
463
+ }
464
+ return new StorageBucketClient(bucketId, this._http);
465
+ }
466
+ };
467
+ var StorageBucketClient = class {
468
+ constructor(_bucketId, _http) {
469
+ this._bucketId = _bucketId;
470
+ this._http = _http;
471
+ }
472
+ /**
473
+ * Liste les fichiers du bucket avec pagination et filtre préfixe.
474
+ *
475
+ * - Bucket **public** : Bearer non requis — retourne tous les fichiers.
476
+ * - Bucket **privé** : Bearer requis — retourne uniquement les fichiers du user connecté.
477
+ *
478
+ * @example
479
+ * const { data, meta } = await bucket.listObjects({ prefix: 'users/123/', perPage: 20 })
480
+ */
481
+ async listObjects(options = {}) {
482
+ const params = {};
483
+ if (options.page !== void 0) params["page"] = options.page;
484
+ if (options.perPage !== void 0) params["per_page"] = options.perPage;
485
+ if (options.prefix) params["prefix"] = options.prefix;
486
+ return this._http.get(
487
+ `/sdk/storage/buckets/${this._bucketId}/objects`,
488
+ params
489
+ );
490
+ }
491
+ /**
492
+ * Upload un fichier en 3 étapes : présignature → PUT direct vers OVH → confirmation.
493
+ *
494
+ * Bearer requis. La progression PUT (0–100 %) peut être suivie via `options.onProgress`.
495
+ *
496
+ * @param file Fichier ou Blob à uploader
497
+ * @param name Nom dans le bucket (ex: "photo.jpg", "users/123/avatar.jpg")
498
+ * @param options contentType (override), onProgress
499
+ *
500
+ * @example
501
+ * const obj = await bucket.upload(file, 'avatar.jpg', {
502
+ * onProgress: pct => console.log(`${pct}%`)
503
+ * })
504
+ */
505
+ async upload(file, name, options = {}) {
506
+ const contentType = options.contentType ?? ((file instanceof File ? file.type : "") || "application/octet-stream");
507
+ const presign = await this._http.post(`/sdk/storage/buckets/${this._bucketId}/upload`, {
508
+ name,
509
+ content_type: contentType,
510
+ size: file.size
511
+ });
512
+ await this._putToOvh(presign.upload_url, file, contentType, options.onProgress);
513
+ const confirm = await this._http.post(
514
+ `/sdk/storage/buckets/${this._bucketId}/confirm`,
515
+ { name, key: presign.key, content_type: contentType }
516
+ );
517
+ return confirm.data;
518
+ }
519
+ /**
520
+ * Retourne l'URL d'accès à un fichier.
521
+ *
522
+ * - Bucket **public** : URL publique permanente, Bearer non requis.
523
+ * - Bucket **privé** : URL signée (TTL 3600 s), Bearer requis, créateur uniquement.
524
+ */
525
+ async getUrl(objectId) {
526
+ return this._http.get(
527
+ `/sdk/storage/buckets/${this._bucketId}/objects/${objectId}/url`
528
+ );
529
+ }
530
+ /**
531
+ * Supprime un fichier du bucket.
532
+ * Bearer requis. Seul le créateur du fichier peut le supprimer.
533
+ */
534
+ async delete(objectId) {
535
+ await this._http.delete(`/sdk/storage/buckets/${this._bucketId}/objects/${objectId}`);
536
+ }
537
+ // ── Private ──────────────────────────────────────────────────────────────
538
+ /**
539
+ * PUT direct vers l'URL présignée OVH.
540
+ * Utilise XHR (si disponible) pour le suivi de progression, sinon fetch.
541
+ */
542
+ async _putToOvh(url, file, contentType, onProgress) {
543
+ if (typeof onProgress === "function" && typeof XMLHttpRequest !== "undefined") {
544
+ return new Promise((resolve, reject) => {
545
+ const xhr = new XMLHttpRequest();
546
+ xhr.open("PUT", url);
547
+ xhr.setRequestHeader("Content-Type", contentType);
548
+ xhr.upload.addEventListener("progress", (e) => {
549
+ if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
550
+ });
551
+ xhr.addEventListener("load", () => {
552
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
553
+ else reject(new NetworkError(`Upload OVH \xE9chou\xE9 (${xhr.status})`));
554
+ });
555
+ xhr.addEventListener(
556
+ "error",
557
+ () => reject(new NetworkError("Erreur r\xE9seau lors de l'upload OVH."))
558
+ );
559
+ xhr.send(file);
560
+ });
561
+ }
562
+ const res = await fetch(url, {
563
+ method: "PUT",
564
+ headers: { "Content-Type": contentType },
565
+ body: file
566
+ });
567
+ if (!res.ok) throw new NetworkError(`Upload OVH \xE9chou\xE9 (${res.status})`);
568
+ }
569
+ };
570
+
416
571
  // src/token.ts
417
572
  var KEYS = {
418
573
  access: "frenchbaas_access_token",
@@ -527,6 +682,7 @@ var FrenchBaasClient = class {
527
682
  this._tokenStore = new TokenStore(config.storage ?? "memory");
528
683
  this._http = new HttpClient(baseUrl, config.apiKey, this._tokenStore);
529
684
  this.auth = new AuthModule(this._http, this._tokenStore, baseUrl, config.apiKey);
685
+ this.storage = new StorageModule(this._http);
530
686
  }
531
687
  /**
532
688
  * Retourne un client pour une collection spécifique.
@@ -558,6 +714,7 @@ var FrenchBaasClient = class {
558
714
  // Annotate the CommonJS export names for ESM import in node:
559
715
  0 && (module.exports = {
560
716
  AuthError,
717
+ ConflictError,
561
718
  FrenchBaas,
562
719
  FrenchBaasError,
563
720
  NetworkError,
package/dist/index.d.mts CHANGED
@@ -49,13 +49,20 @@ interface GetOptions {
49
49
  perPage?: number;
50
50
  /**
51
51
  * Filtres sur les champs du document.
52
- * - Champ string/number/boolean : égalité exacte
53
- * - Champ references : l'UUID doit être présent dans le tableau
54
52
  *
55
- * @example { status: 'pending' }
56
- * @example { category_ids: '3e0a5843-...' }
53
+ * - Valeur unique (string) → égalité exacte ou contient (references/array)
54
+ * - Tableau string[] → OR : field = v1 OU v2 / contient v1 OU v2
55
+ * - { and: string[] } → AND : uniquement pour references/array — contient tous les éléments
56
+ *
57
+ * @example { status: 'active' }
58
+ * @example { status: ['active', 'pending'] }
59
+ * @example { category_ids: 'uuid1' }
60
+ * @example { category_ids: ['uuid1', 'uuid2'] }
61
+ * @example { category_ids: { and: ['uuid1', 'uuid2'] } }
57
62
  */
58
- filter?: Record<string, string>;
63
+ filter?: Record<string, string | string[] | {
64
+ and: string[];
65
+ }>;
59
66
  }
60
67
  type FieldType = 'string' | 'integer' | 'number' | 'boolean' | 'array' | 'object' | 'datetime';
61
68
  interface SchemaField {
@@ -70,6 +77,44 @@ interface CollectionSchema {
70
77
  visibility: 'public' | 'authenticated' | 'private';
71
78
  schema: SchemaField[];
72
79
  }
80
+ interface StorageBucket {
81
+ id: string;
82
+ name: string;
83
+ visibility: 'public' | 'private';
84
+ public: boolean;
85
+ created_at: string;
86
+ }
87
+ interface StorageObject {
88
+ id: string;
89
+ name: string;
90
+ content_type: string;
91
+ size: number;
92
+ created_by: string | null;
93
+ created_at: string;
94
+ /** URL publique — présente uniquement si le bucket est public */
95
+ url?: string;
96
+ }
97
+ interface StorageListOptions {
98
+ /** Numéro de page (défaut: 1) */
99
+ page?: number;
100
+ /** Fichiers par page — max 100 (défaut: 50) */
101
+ perPage?: number;
102
+ /** Filtre les fichiers dont le nom commence par ce préfixe */
103
+ prefix?: string;
104
+ }
105
+ interface StorageListResult {
106
+ data: StorageObject[];
107
+ meta: PaginationMeta;
108
+ }
109
+ interface StorageUploadOptions {
110
+ /** Force le Content-Type (sinon déduit de file.type) */
111
+ contentType?: string;
112
+ /**
113
+ * Callback appelé pendant le PUT vers OVH (0–100).
114
+ * Nécessite XMLHttpRequest (navigateur). Ignoré côté Node/SSR.
115
+ */
116
+ onProgress?: (pct: number) => void;
117
+ }
73
118
  interface OpenApiSpec {
74
119
  openapi: string;
75
120
  info: Record<string, unknown>;
@@ -134,7 +179,7 @@ declare class HttpClient {
134
179
  constructor(_baseUrl: string, _apiKey: string, _tokenStore: TokenStore);
135
180
  /** Enregistre la fonction de refresh (injectée par AuthModule). */
136
181
  setRefreshFn(fn: RefreshFn): void;
137
- get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
182
+ get<T>(path: string, params?: Record<string, string | number | string[] | undefined>): Promise<T>;
138
183
  post<T>(path: string, body?: unknown): Promise<T>;
139
184
  patch<T>(path: string, body?: unknown): Promise<T>;
140
185
  delete<T>(path: string): Promise<T>;
@@ -276,6 +321,93 @@ declare class CollectionClient<T = Record<string, unknown>> {
276
321
  schema(): Promise<CollectionSchema>;
277
322
  }
278
323
 
324
+ /**
325
+ * Module storage — découverte des buckets et accès aux opérations par bucket.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * // Lister les buckets (pas de Bearer requis)
330
+ * const buckets = await client.storage.listBuckets()
331
+ *
332
+ * // Opérations sur un bucket spécifique
333
+ * const bucket = client.storage.bucket('bucket-uuid')
334
+ * const obj = await bucket.upload(file, 'photo.jpg')
335
+ * const list = await bucket.listObjects({ prefix: 'users/123/' })
336
+ * const { url } = await bucket.getUrl(obj.id)
337
+ * await bucket.delete(obj.id)
338
+ * ```
339
+ */
340
+ declare class StorageModule {
341
+ private readonly _http;
342
+ constructor(_http: HttpClient);
343
+ /**
344
+ * Liste tous les buckets du projet.
345
+ *
346
+ * Authentification par API key uniquement — pas de Bearer requis.
347
+ * Usage principal : découverte des IDs pendant la construction de l'app.
348
+ */
349
+ listBuckets(): Promise<StorageBucket[]>;
350
+ /**
351
+ * Retourne un client pour opérer sur un bucket spécifique.
352
+ *
353
+ * @param bucketId UUID du bucket (visible dans Dashboard → Storage)
354
+ */
355
+ bucket(bucketId: string): StorageBucketClient;
356
+ }
357
+ /**
358
+ * Client pour les opérations sur un bucket spécifique.
359
+ */
360
+ declare class StorageBucketClient {
361
+ private readonly _bucketId;
362
+ private readonly _http;
363
+ constructor(_bucketId: string, _http: HttpClient);
364
+ /**
365
+ * Liste les fichiers du bucket avec pagination et filtre préfixe.
366
+ *
367
+ * - Bucket **public** : Bearer non requis — retourne tous les fichiers.
368
+ * - Bucket **privé** : Bearer requis — retourne uniquement les fichiers du user connecté.
369
+ *
370
+ * @example
371
+ * const { data, meta } = await bucket.listObjects({ prefix: 'users/123/', perPage: 20 })
372
+ */
373
+ listObjects(options?: StorageListOptions): Promise<StorageListResult>;
374
+ /**
375
+ * Upload un fichier en 3 étapes : présignature → PUT direct vers OVH → confirmation.
376
+ *
377
+ * Bearer requis. La progression PUT (0–100 %) peut être suivie via `options.onProgress`.
378
+ *
379
+ * @param file Fichier ou Blob à uploader
380
+ * @param name Nom dans le bucket (ex: "photo.jpg", "users/123/avatar.jpg")
381
+ * @param options contentType (override), onProgress
382
+ *
383
+ * @example
384
+ * const obj = await bucket.upload(file, 'avatar.jpg', {
385
+ * onProgress: pct => console.log(`${pct}%`)
386
+ * })
387
+ */
388
+ upload(file: File | Blob, name: string, options?: StorageUploadOptions): Promise<StorageObject>;
389
+ /**
390
+ * Retourne l'URL d'accès à un fichier.
391
+ *
392
+ * - Bucket **public** : URL publique permanente, Bearer non requis.
393
+ * - Bucket **privé** : URL signée (TTL 3600 s), Bearer requis, créateur uniquement.
394
+ */
395
+ getUrl(objectId: string): Promise<{
396
+ url: string;
397
+ expires_in?: number;
398
+ }>;
399
+ /**
400
+ * Supprime un fichier du bucket.
401
+ * Bearer requis. Seul le créateur du fichier peut le supprimer.
402
+ */
403
+ delete(objectId: string): Promise<void>;
404
+ /**
405
+ * PUT direct vers l'URL présignée OVH.
406
+ * Utilise XHR (si disponible) pour le suivi de progression, sinon fetch.
407
+ */
408
+ private _putToOvh;
409
+ }
410
+
279
411
  /**
280
412
  * Client principal FrenchBaas.
281
413
  *
@@ -296,6 +428,8 @@ declare class CollectionClient<T = Record<string, unknown>> {
296
428
  declare class FrenchBaasClient {
297
429
  /** Module d'authentification — signUp, login, logout, getUser */
298
430
  readonly auth: AuthModule;
431
+ /** Module storage — listBuckets, bucket(id).upload / listObjects / getUrl / delete */
432
+ readonly storage: StorageModule;
299
433
  private readonly _http;
300
434
  private readonly _tokenStore;
301
435
  constructor(config: FrenchBaasConfig);
@@ -383,5 +517,13 @@ declare class RateLimitError extends FrenchBaasError {
383
517
  declare class ServerError extends FrenchBaasError {
384
518
  constructor(message?: string, status?: number);
385
519
  }
520
+ /**
521
+ * Conflit de ressource — 409.
522
+ * Levée sur storage : un fichier avec ce nom existe déjà dans le bucket,
523
+ * déposé par un autre utilisateur.
524
+ */
525
+ declare class ConflictError extends FrenchBaasError {
526
+ constructor(message?: string);
527
+ }
386
528
 
387
- export { AuthError, type AuthResult, type CollectionSchema, type Document, type DocumentsPage, type FieldType, FrenchBaasClient as FrenchBaas, type FrenchBaasConfig, FrenchBaasError, type GetOptions, NetworkError, NotFoundError, type OpenApiSpec, type PaginationMeta, QuotaError, RateLimitError, type RefreshResult, type SchemaField, ServerError, type User, ValidationError };
529
+ export { AuthError, type AuthResult, type CollectionSchema, ConflictError, type Document, type DocumentsPage, type FieldType, FrenchBaasClient as FrenchBaas, type FrenchBaasConfig, FrenchBaasError, type GetOptions, NetworkError, NotFoundError, type OpenApiSpec, type PaginationMeta, QuotaError, RateLimitError, type RefreshResult, type SchemaField, ServerError, type StorageBucket, type StorageListOptions, type StorageListResult, type StorageObject, type StorageUploadOptions, type User, ValidationError };
package/dist/index.d.ts CHANGED
@@ -49,13 +49,20 @@ interface GetOptions {
49
49
  perPage?: number;
50
50
  /**
51
51
  * Filtres sur les champs du document.
52
- * - Champ string/number/boolean : égalité exacte
53
- * - Champ references : l'UUID doit être présent dans le tableau
54
52
  *
55
- * @example { status: 'pending' }
56
- * @example { category_ids: '3e0a5843-...' }
53
+ * - Valeur unique (string) → égalité exacte ou contient (references/array)
54
+ * - Tableau string[] → OR : field = v1 OU v2 / contient v1 OU v2
55
+ * - { and: string[] } → AND : uniquement pour references/array — contient tous les éléments
56
+ *
57
+ * @example { status: 'active' }
58
+ * @example { status: ['active', 'pending'] }
59
+ * @example { category_ids: 'uuid1' }
60
+ * @example { category_ids: ['uuid1', 'uuid2'] }
61
+ * @example { category_ids: { and: ['uuid1', 'uuid2'] } }
57
62
  */
58
- filter?: Record<string, string>;
63
+ filter?: Record<string, string | string[] | {
64
+ and: string[];
65
+ }>;
59
66
  }
60
67
  type FieldType = 'string' | 'integer' | 'number' | 'boolean' | 'array' | 'object' | 'datetime';
61
68
  interface SchemaField {
@@ -70,6 +77,44 @@ interface CollectionSchema {
70
77
  visibility: 'public' | 'authenticated' | 'private';
71
78
  schema: SchemaField[];
72
79
  }
80
+ interface StorageBucket {
81
+ id: string;
82
+ name: string;
83
+ visibility: 'public' | 'private';
84
+ public: boolean;
85
+ created_at: string;
86
+ }
87
+ interface StorageObject {
88
+ id: string;
89
+ name: string;
90
+ content_type: string;
91
+ size: number;
92
+ created_by: string | null;
93
+ created_at: string;
94
+ /** URL publique — présente uniquement si le bucket est public */
95
+ url?: string;
96
+ }
97
+ interface StorageListOptions {
98
+ /** Numéro de page (défaut: 1) */
99
+ page?: number;
100
+ /** Fichiers par page — max 100 (défaut: 50) */
101
+ perPage?: number;
102
+ /** Filtre les fichiers dont le nom commence par ce préfixe */
103
+ prefix?: string;
104
+ }
105
+ interface StorageListResult {
106
+ data: StorageObject[];
107
+ meta: PaginationMeta;
108
+ }
109
+ interface StorageUploadOptions {
110
+ /** Force le Content-Type (sinon déduit de file.type) */
111
+ contentType?: string;
112
+ /**
113
+ * Callback appelé pendant le PUT vers OVH (0–100).
114
+ * Nécessite XMLHttpRequest (navigateur). Ignoré côté Node/SSR.
115
+ */
116
+ onProgress?: (pct: number) => void;
117
+ }
73
118
  interface OpenApiSpec {
74
119
  openapi: string;
75
120
  info: Record<string, unknown>;
@@ -134,7 +179,7 @@ declare class HttpClient {
134
179
  constructor(_baseUrl: string, _apiKey: string, _tokenStore: TokenStore);
135
180
  /** Enregistre la fonction de refresh (injectée par AuthModule). */
136
181
  setRefreshFn(fn: RefreshFn): void;
137
- get<T>(path: string, params?: Record<string, string | number | undefined>): Promise<T>;
182
+ get<T>(path: string, params?: Record<string, string | number | string[] | undefined>): Promise<T>;
138
183
  post<T>(path: string, body?: unknown): Promise<T>;
139
184
  patch<T>(path: string, body?: unknown): Promise<T>;
140
185
  delete<T>(path: string): Promise<T>;
@@ -276,6 +321,93 @@ declare class CollectionClient<T = Record<string, unknown>> {
276
321
  schema(): Promise<CollectionSchema>;
277
322
  }
278
323
 
324
+ /**
325
+ * Module storage — découverte des buckets et accès aux opérations par bucket.
326
+ *
327
+ * @example
328
+ * ```ts
329
+ * // Lister les buckets (pas de Bearer requis)
330
+ * const buckets = await client.storage.listBuckets()
331
+ *
332
+ * // Opérations sur un bucket spécifique
333
+ * const bucket = client.storage.bucket('bucket-uuid')
334
+ * const obj = await bucket.upload(file, 'photo.jpg')
335
+ * const list = await bucket.listObjects({ prefix: 'users/123/' })
336
+ * const { url } = await bucket.getUrl(obj.id)
337
+ * await bucket.delete(obj.id)
338
+ * ```
339
+ */
340
+ declare class StorageModule {
341
+ private readonly _http;
342
+ constructor(_http: HttpClient);
343
+ /**
344
+ * Liste tous les buckets du projet.
345
+ *
346
+ * Authentification par API key uniquement — pas de Bearer requis.
347
+ * Usage principal : découverte des IDs pendant la construction de l'app.
348
+ */
349
+ listBuckets(): Promise<StorageBucket[]>;
350
+ /**
351
+ * Retourne un client pour opérer sur un bucket spécifique.
352
+ *
353
+ * @param bucketId UUID du bucket (visible dans Dashboard → Storage)
354
+ */
355
+ bucket(bucketId: string): StorageBucketClient;
356
+ }
357
+ /**
358
+ * Client pour les opérations sur un bucket spécifique.
359
+ */
360
+ declare class StorageBucketClient {
361
+ private readonly _bucketId;
362
+ private readonly _http;
363
+ constructor(_bucketId: string, _http: HttpClient);
364
+ /**
365
+ * Liste les fichiers du bucket avec pagination et filtre préfixe.
366
+ *
367
+ * - Bucket **public** : Bearer non requis — retourne tous les fichiers.
368
+ * - Bucket **privé** : Bearer requis — retourne uniquement les fichiers du user connecté.
369
+ *
370
+ * @example
371
+ * const { data, meta } = await bucket.listObjects({ prefix: 'users/123/', perPage: 20 })
372
+ */
373
+ listObjects(options?: StorageListOptions): Promise<StorageListResult>;
374
+ /**
375
+ * Upload un fichier en 3 étapes : présignature → PUT direct vers OVH → confirmation.
376
+ *
377
+ * Bearer requis. La progression PUT (0–100 %) peut être suivie via `options.onProgress`.
378
+ *
379
+ * @param file Fichier ou Blob à uploader
380
+ * @param name Nom dans le bucket (ex: "photo.jpg", "users/123/avatar.jpg")
381
+ * @param options contentType (override), onProgress
382
+ *
383
+ * @example
384
+ * const obj = await bucket.upload(file, 'avatar.jpg', {
385
+ * onProgress: pct => console.log(`${pct}%`)
386
+ * })
387
+ */
388
+ upload(file: File | Blob, name: string, options?: StorageUploadOptions): Promise<StorageObject>;
389
+ /**
390
+ * Retourne l'URL d'accès à un fichier.
391
+ *
392
+ * - Bucket **public** : URL publique permanente, Bearer non requis.
393
+ * - Bucket **privé** : URL signée (TTL 3600 s), Bearer requis, créateur uniquement.
394
+ */
395
+ getUrl(objectId: string): Promise<{
396
+ url: string;
397
+ expires_in?: number;
398
+ }>;
399
+ /**
400
+ * Supprime un fichier du bucket.
401
+ * Bearer requis. Seul le créateur du fichier peut le supprimer.
402
+ */
403
+ delete(objectId: string): Promise<void>;
404
+ /**
405
+ * PUT direct vers l'URL présignée OVH.
406
+ * Utilise XHR (si disponible) pour le suivi de progression, sinon fetch.
407
+ */
408
+ private _putToOvh;
409
+ }
410
+
279
411
  /**
280
412
  * Client principal FrenchBaas.
281
413
  *
@@ -296,6 +428,8 @@ declare class CollectionClient<T = Record<string, unknown>> {
296
428
  declare class FrenchBaasClient {
297
429
  /** Module d'authentification — signUp, login, logout, getUser */
298
430
  readonly auth: AuthModule;
431
+ /** Module storage — listBuckets, bucket(id).upload / listObjects / getUrl / delete */
432
+ readonly storage: StorageModule;
299
433
  private readonly _http;
300
434
  private readonly _tokenStore;
301
435
  constructor(config: FrenchBaasConfig);
@@ -383,5 +517,13 @@ declare class RateLimitError extends FrenchBaasError {
383
517
  declare class ServerError extends FrenchBaasError {
384
518
  constructor(message?: string, status?: number);
385
519
  }
520
+ /**
521
+ * Conflit de ressource — 409.
522
+ * Levée sur storage : un fichier avec ce nom existe déjà dans le bucket,
523
+ * déposé par un autre utilisateur.
524
+ */
525
+ declare class ConflictError extends FrenchBaasError {
526
+ constructor(message?: string);
527
+ }
386
528
 
387
- export { AuthError, type AuthResult, type CollectionSchema, type Document, type DocumentsPage, type FieldType, FrenchBaasClient as FrenchBaas, type FrenchBaasConfig, FrenchBaasError, type GetOptions, NetworkError, NotFoundError, type OpenApiSpec, type PaginationMeta, QuotaError, RateLimitError, type RefreshResult, type SchemaField, ServerError, type User, ValidationError };
529
+ export { AuthError, type AuthResult, type CollectionSchema, ConflictError, type Document, type DocumentsPage, type FieldType, FrenchBaasClient as FrenchBaas, type FrenchBaasConfig, FrenchBaasError, type GetOptions, NetworkError, NotFoundError, type OpenApiSpec, type PaginationMeta, QuotaError, RateLimitError, type RefreshResult, type SchemaField, ServerError, type StorageBucket, type StorageListOptions, type StorageListResult, type StorageObject, type StorageUploadOptions, type User, ValidationError };
package/dist/index.js CHANGED
@@ -57,6 +57,13 @@ var ServerError = class extends FrenchBaasError {
57
57
  Object.setPrototypeOf(this, new.target.prototype);
58
58
  }
59
59
  };
60
+ var ConflictError = class extends FrenchBaasError {
61
+ constructor(message = "Conflit : la ressource existe d\xE9j\xE0.") {
62
+ super(message, 409);
63
+ this.name = "ConflictError";
64
+ Object.setPrototypeOf(this, new.target.prototype);
65
+ }
66
+ };
60
67
 
61
68
  // src/auth.ts
62
69
  var AuthModule = class {
@@ -177,7 +184,14 @@ var CollectionClient = class {
177
184
  };
178
185
  if (options.filter) {
179
186
  for (const [key, val] of Object.entries(options.filter)) {
180
- if (val !== void 0) params[`filter[${key}]`] = val;
187
+ if (val === void 0) continue;
188
+ if (typeof val === "string") {
189
+ params[`filter[${key}]`] = val;
190
+ } else if (Array.isArray(val)) {
191
+ params[`filter[${key}][]`] = val;
192
+ } else {
193
+ params[`filter[${key}][and][]`] = val.and;
194
+ }
181
195
  }
182
196
  }
183
197
  const res = await this._http.get(
@@ -280,7 +294,12 @@ var HttpClient = class {
280
294
  const url = new URL(this._baseUrl + path);
281
295
  if (params) {
282
296
  for (const [k, v] of Object.entries(params)) {
283
- if (v !== void 0) url.searchParams.set(k, String(v));
297
+ if (v === void 0) continue;
298
+ if (Array.isArray(v)) {
299
+ for (const item of v) url.searchParams.append(k, item);
300
+ } else {
301
+ url.searchParams.set(k, String(v));
302
+ }
284
303
  }
285
304
  }
286
305
  return this._request("GET", url.toString(), void 0, true);
@@ -370,6 +389,8 @@ var HttpClient = class {
370
389
  const details = err.errors ?? [];
371
390
  throw new ValidationError(details.length ? details.join(", ") : msg, details);
372
391
  }
392
+ case 409:
393
+ throw new ConflictError(msg);
373
394
  case 429:
374
395
  throw new RateLimitError();
375
396
  default:
@@ -379,6 +400,139 @@ var HttpClient = class {
379
400
  }
380
401
  };
381
402
 
403
+ // src/storage.ts
404
+ var StorageModule = class {
405
+ constructor(_http) {
406
+ this._http = _http;
407
+ }
408
+ /**
409
+ * Liste tous les buckets du projet.
410
+ *
411
+ * Authentification par API key uniquement — pas de Bearer requis.
412
+ * Usage principal : découverte des IDs pendant la construction de l'app.
413
+ */
414
+ async listBuckets() {
415
+ const res = await this._http.get(
416
+ "/sdk/storage/buckets"
417
+ );
418
+ return res.data;
419
+ }
420
+ /**
421
+ * Retourne un client pour opérer sur un bucket spécifique.
422
+ *
423
+ * @param bucketId UUID du bucket (visible dans Dashboard → Storage)
424
+ */
425
+ bucket(bucketId) {
426
+ if (!bucketId || typeof bucketId !== "string") {
427
+ throw new Error("[FrenchBaas] storage.bucket() requiert un UUID de bucket valide.");
428
+ }
429
+ return new StorageBucketClient(bucketId, this._http);
430
+ }
431
+ };
432
+ var StorageBucketClient = class {
433
+ constructor(_bucketId, _http) {
434
+ this._bucketId = _bucketId;
435
+ this._http = _http;
436
+ }
437
+ /**
438
+ * Liste les fichiers du bucket avec pagination et filtre préfixe.
439
+ *
440
+ * - Bucket **public** : Bearer non requis — retourne tous les fichiers.
441
+ * - Bucket **privé** : Bearer requis — retourne uniquement les fichiers du user connecté.
442
+ *
443
+ * @example
444
+ * const { data, meta } = await bucket.listObjects({ prefix: 'users/123/', perPage: 20 })
445
+ */
446
+ async listObjects(options = {}) {
447
+ const params = {};
448
+ if (options.page !== void 0) params["page"] = options.page;
449
+ if (options.perPage !== void 0) params["per_page"] = options.perPage;
450
+ if (options.prefix) params["prefix"] = options.prefix;
451
+ return this._http.get(
452
+ `/sdk/storage/buckets/${this._bucketId}/objects`,
453
+ params
454
+ );
455
+ }
456
+ /**
457
+ * Upload un fichier en 3 étapes : présignature → PUT direct vers OVH → confirmation.
458
+ *
459
+ * Bearer requis. La progression PUT (0–100 %) peut être suivie via `options.onProgress`.
460
+ *
461
+ * @param file Fichier ou Blob à uploader
462
+ * @param name Nom dans le bucket (ex: "photo.jpg", "users/123/avatar.jpg")
463
+ * @param options contentType (override), onProgress
464
+ *
465
+ * @example
466
+ * const obj = await bucket.upload(file, 'avatar.jpg', {
467
+ * onProgress: pct => console.log(`${pct}%`)
468
+ * })
469
+ */
470
+ async upload(file, name, options = {}) {
471
+ const contentType = options.contentType ?? ((file instanceof File ? file.type : "") || "application/octet-stream");
472
+ const presign = await this._http.post(`/sdk/storage/buckets/${this._bucketId}/upload`, {
473
+ name,
474
+ content_type: contentType,
475
+ size: file.size
476
+ });
477
+ await this._putToOvh(presign.upload_url, file, contentType, options.onProgress);
478
+ const confirm = await this._http.post(
479
+ `/sdk/storage/buckets/${this._bucketId}/confirm`,
480
+ { name, key: presign.key, content_type: contentType }
481
+ );
482
+ return confirm.data;
483
+ }
484
+ /**
485
+ * Retourne l'URL d'accès à un fichier.
486
+ *
487
+ * - Bucket **public** : URL publique permanente, Bearer non requis.
488
+ * - Bucket **privé** : URL signée (TTL 3600 s), Bearer requis, créateur uniquement.
489
+ */
490
+ async getUrl(objectId) {
491
+ return this._http.get(
492
+ `/sdk/storage/buckets/${this._bucketId}/objects/${objectId}/url`
493
+ );
494
+ }
495
+ /**
496
+ * Supprime un fichier du bucket.
497
+ * Bearer requis. Seul le créateur du fichier peut le supprimer.
498
+ */
499
+ async delete(objectId) {
500
+ await this._http.delete(`/sdk/storage/buckets/${this._bucketId}/objects/${objectId}`);
501
+ }
502
+ // ── Private ──────────────────────────────────────────────────────────────
503
+ /**
504
+ * PUT direct vers l'URL présignée OVH.
505
+ * Utilise XHR (si disponible) pour le suivi de progression, sinon fetch.
506
+ */
507
+ async _putToOvh(url, file, contentType, onProgress) {
508
+ if (typeof onProgress === "function" && typeof XMLHttpRequest !== "undefined") {
509
+ return new Promise((resolve, reject) => {
510
+ const xhr = new XMLHttpRequest();
511
+ xhr.open("PUT", url);
512
+ xhr.setRequestHeader("Content-Type", contentType);
513
+ xhr.upload.addEventListener("progress", (e) => {
514
+ if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100));
515
+ });
516
+ xhr.addEventListener("load", () => {
517
+ if (xhr.status >= 200 && xhr.status < 300) resolve();
518
+ else reject(new NetworkError(`Upload OVH \xE9chou\xE9 (${xhr.status})`));
519
+ });
520
+ xhr.addEventListener(
521
+ "error",
522
+ () => reject(new NetworkError("Erreur r\xE9seau lors de l'upload OVH."))
523
+ );
524
+ xhr.send(file);
525
+ });
526
+ }
527
+ const res = await fetch(url, {
528
+ method: "PUT",
529
+ headers: { "Content-Type": contentType },
530
+ body: file
531
+ });
532
+ if (!res.ok) throw new NetworkError(`Upload OVH \xE9chou\xE9 (${res.status})`);
533
+ }
534
+ };
535
+
382
536
  // src/token.ts
383
537
  var KEYS = {
384
538
  access: "frenchbaas_access_token",
@@ -493,6 +647,7 @@ var FrenchBaasClient = class {
493
647
  this._tokenStore = new TokenStore(config.storage ?? "memory");
494
648
  this._http = new HttpClient(baseUrl, config.apiKey, this._tokenStore);
495
649
  this.auth = new AuthModule(this._http, this._tokenStore, baseUrl, config.apiKey);
650
+ this.storage = new StorageModule(this._http);
496
651
  }
497
652
  /**
498
653
  * Retourne un client pour une collection spécifique.
@@ -523,6 +678,7 @@ var FrenchBaasClient = class {
523
678
  };
524
679
  export {
525
680
  AuthError,
681
+ ConflictError,
526
682
  FrenchBaasClient as FrenchBaas,
527
683
  FrenchBaasError,
528
684
  NetworkError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frenchbaas/js",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "SDK JavaScript officiel pour FrenchBaas",
5
5
  "author": "FrenchBaas",
6
6
  "license": "MIT",