@frenchbaas/js 0.3.1 → 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
@@ -119,6 +119,61 @@ Ces permissions se configurent dans le Dashboard → collection → schéma →
119
119
 
120
120
  ---
121
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
+
122
177
  ## Webhooks
123
178
 
124
179
  Si une collection a un `before_create_url` ou `before_update_url` configuré,
@@ -162,6 +217,7 @@ try {
162
217
  import {
163
218
  FrenchBaasError,
164
219
  AuthError,
220
+ ConflictError,
165
221
  ValidationError,
166
222
  NotFoundError,
167
223
  QuotaError,
@@ -180,6 +236,7 @@ try {
180
236
  console.error(e.message, e.errors)
181
237
  }
182
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')
183
240
  else if (e instanceof NotFoundError) console.error('Document ou collection introuvable')
184
241
  else if (e instanceof QuotaError) console.error('Quota dépassé :', e.message)
185
242
  else if (e instanceof RateLimitError) console.error('Trop de requêtes, réessayez dans quelques instants')
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 {
@@ -416,6 +424,8 @@ var HttpClient = class {
416
424
  const details = err.errors ?? [];
417
425
  throw new ValidationError(details.length ? details.join(", ") : msg, details);
418
426
  }
427
+ case 409:
428
+ throw new ConflictError(msg);
419
429
  case 429:
420
430
  throw new RateLimitError();
421
431
  default:
@@ -425,6 +435,139 @@ var HttpClient = class {
425
435
  }
426
436
  };
427
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
+
428
571
  // src/token.ts
429
572
  var KEYS = {
430
573
  access: "frenchbaas_access_token",
@@ -539,6 +682,7 @@ var FrenchBaasClient = class {
539
682
  this._tokenStore = new TokenStore(config.storage ?? "memory");
540
683
  this._http = new HttpClient(baseUrl, config.apiKey, this._tokenStore);
541
684
  this.auth = new AuthModule(this._http, this._tokenStore, baseUrl, config.apiKey);
685
+ this.storage = new StorageModule(this._http);
542
686
  }
543
687
  /**
544
688
  * Retourne un client pour une collection spécifique.
@@ -570,6 +714,7 @@ var FrenchBaasClient = class {
570
714
  // Annotate the CommonJS export names for ESM import in node:
571
715
  0 && (module.exports = {
572
716
  AuthError,
717
+ ConflictError,
573
718
  FrenchBaas,
574
719
  FrenchBaasError,
575
720
  NetworkError,
package/dist/index.d.mts CHANGED
@@ -77,6 +77,44 @@ interface CollectionSchema {
77
77
  visibility: 'public' | 'authenticated' | 'private';
78
78
  schema: SchemaField[];
79
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
+ }
80
118
  interface OpenApiSpec {
81
119
  openapi: string;
82
120
  info: Record<string, unknown>;
@@ -283,6 +321,93 @@ declare class CollectionClient<T = Record<string, unknown>> {
283
321
  schema(): Promise<CollectionSchema>;
284
322
  }
285
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
+
286
411
  /**
287
412
  * Client principal FrenchBaas.
288
413
  *
@@ -303,6 +428,8 @@ declare class CollectionClient<T = Record<string, unknown>> {
303
428
  declare class FrenchBaasClient {
304
429
  /** Module d'authentification — signUp, login, logout, getUser */
305
430
  readonly auth: AuthModule;
431
+ /** Module storage — listBuckets, bucket(id).upload / listObjects / getUrl / delete */
432
+ readonly storage: StorageModule;
306
433
  private readonly _http;
307
434
  private readonly _tokenStore;
308
435
  constructor(config: FrenchBaasConfig);
@@ -390,5 +517,13 @@ declare class RateLimitError extends FrenchBaasError {
390
517
  declare class ServerError extends FrenchBaasError {
391
518
  constructor(message?: string, status?: number);
392
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
+ }
393
528
 
394
- 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
@@ -77,6 +77,44 @@ interface CollectionSchema {
77
77
  visibility: 'public' | 'authenticated' | 'private';
78
78
  schema: SchemaField[];
79
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
+ }
80
118
  interface OpenApiSpec {
81
119
  openapi: string;
82
120
  info: Record<string, unknown>;
@@ -283,6 +321,93 @@ declare class CollectionClient<T = Record<string, unknown>> {
283
321
  schema(): Promise<CollectionSchema>;
284
322
  }
285
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
+
286
411
  /**
287
412
  * Client principal FrenchBaas.
288
413
  *
@@ -303,6 +428,8 @@ declare class CollectionClient<T = Record<string, unknown>> {
303
428
  declare class FrenchBaasClient {
304
429
  /** Module d'authentification — signUp, login, logout, getUser */
305
430
  readonly auth: AuthModule;
431
+ /** Module storage — listBuckets, bucket(id).upload / listObjects / getUrl / delete */
432
+ readonly storage: StorageModule;
306
433
  private readonly _http;
307
434
  private readonly _tokenStore;
308
435
  constructor(config: FrenchBaasConfig);
@@ -390,5 +517,13 @@ declare class RateLimitError extends FrenchBaasError {
390
517
  declare class ServerError extends FrenchBaasError {
391
518
  constructor(message?: string, status?: number);
392
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
+ }
393
528
 
394
- 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 {
@@ -382,6 +389,8 @@ var HttpClient = class {
382
389
  const details = err.errors ?? [];
383
390
  throw new ValidationError(details.length ? details.join(", ") : msg, details);
384
391
  }
392
+ case 409:
393
+ throw new ConflictError(msg);
385
394
  case 429:
386
395
  throw new RateLimitError();
387
396
  default:
@@ -391,6 +400,139 @@ var HttpClient = class {
391
400
  }
392
401
  };
393
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
+
394
536
  // src/token.ts
395
537
  var KEYS = {
396
538
  access: "frenchbaas_access_token",
@@ -505,6 +647,7 @@ var FrenchBaasClient = class {
505
647
  this._tokenStore = new TokenStore(config.storage ?? "memory");
506
648
  this._http = new HttpClient(baseUrl, config.apiKey, this._tokenStore);
507
649
  this.auth = new AuthModule(this._http, this._tokenStore, baseUrl, config.apiKey);
650
+ this.storage = new StorageModule(this._http);
508
651
  }
509
652
  /**
510
653
  * Retourne un client pour une collection spécifique.
@@ -535,6 +678,7 @@ var FrenchBaasClient = class {
535
678
  };
536
679
  export {
537
680
  AuthError,
681
+ ConflictError,
538
682
  FrenchBaasClient as FrenchBaas,
539
683
  FrenchBaasError,
540
684
  NetworkError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frenchbaas/js",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "SDK JavaScript officiel pour FrenchBaas",
5
5
  "author": "FrenchBaas",
6
6
  "license": "MIT",