npm package: @frenchbaas/js
Description: SDK JavaScript officiel pour FrenchBaas
Author: FrenchBaas
Latest version: 0.4.2

# @frenchbaas/js

SDK JavaScript officiel pour [FrenchBaas](https://frenchbaas.fr) — le BaaS français.

## Installation

```bash
npm install @frenchbaas/js
```

## Démarrage rapide

```js
import { FrenchBaas } from '@frenchbaas/js'

const client = new FrenchBaas({
  url:    'https://app.frenchbaas.com',
  apiKey: 'votre-clé-api',         // trouvée dans Dashboard → Projet
})
```

---

## Auth

```js
// Inscription (mot de passe : 8 caractères minimum)
const { user, access_token } = await client.auth.signUp({ email: 'a@b.com', password: 'monMotDePasse' })

// Connexion
const { user, access_token } = await client.auth.login({ email: 'a@b.com', password: 'monMotDePasse' })

// Déconnexion
await client.auth.logout()

// État de la session
client.auth.getUser()     // { id, email } | null
client.auth.isLoggedIn()  // boolean
```

---

## Collections & documents

```js
const col = client.collection('uuid-de-la-collection')

// Lister (paginé, max 100 par page)
const { data, meta } = await col.get({ page: 1, perPage: 20 })
// meta → { total, page, per_page, total_pages }

// Filtrer par champ — valeur unique
const { data: pending } = await col.get({ filter: { status: 'pending' } })

// Filtrer OR — plusieurs valeurs (status pending OU delivered)
const { data: multi } = await col.get({ filter: { status: ['pending', 'delivered'] } })

// Filtrer par references — valeur unique (contient l'UUID)
const { data: products } = await col.get({ filter: { category_ids: 'uuid-categorie' } })

// Filtrer references OR — catégorie A OU B
const { data: orProducts } = await col.get({ filter: { category_ids: ['uuid-cat-a', 'uuid-cat-b'] } })

// Filtrer references AND — dans catégorie A ET B (uniquement pour references/array)
const { data: andProducts } = await col.get({ filter: { category_ids: { and: ['uuid-cat-a', 'uuid-cat-b'] } } })

// ⚠ Maximum 20 valeurs par filtre (OR ou AND)

// Récupérer un document par ID
const doc = await col.getById('doc-uuid')
// doc → { id, data, created_at, updated_at }

// Créer un document
const doc = await col.create({ title: 'Hello', published: true })

// Mettre à jour (merge partiel — seuls les champs fournis changent)
const doc = await col.update('doc-uuid', { title: 'Modifié' })

// Supprimer
await col.delete('doc-uuid')

// Schéma de la collection (noms de champs, types, visibilité)
const { schema } = await col.schema()
```

### Visibilité des collections

| Visibilité | Lecture | Écriture |
|------------|---------|---------|
| `public` | Sans token | Token requis |
| `authenticated` | Token requis | Token requis |
| `private` | Créateur uniquement | Créateur uniquement |

### Permissions SDK sur les champs

Le développeur peut configurer une permission sur chaque champ d'une collection.
Ces règles sont appliquées **côté serveur**, de façon transparente pour le SDK.

| Permission | Comportement à la création | Comportement à la mise à jour |
|---|---|---|
| `writable` | L'utilisateur écrit librement **(défaut)** | Modifiable |
| `forced` | Remplacé par la valeur définie par le dev | Ignoré (non modifiable) |
| `inject_user_id` | Injecté automatiquement avec l'ID de l'utilisateur | Ignoré |
| `inject_user_email` | Injecté automatiquement avec l'email de l'utilisateur | Ignoré |
| `inject_timestamp` | Injecté automatiquement avec la date/heure serveur | Ignoré |
| `readonly` | Ignoré (seul le dev peut écrire ce champ) | Ignoré |

```js
// Même si vous envoyez role: 'admin', le serveur force 'client' si le champ est en mode forced
await client.collection('users').create({ role: 'admin', email: 'x@x.com' })
// → { role: 'client', email: 'x@x.com' }

// Les champs inject_* n'ont pas besoin d'être envoyés
await client.collection('orders').create({ total: 99.99 })
// → { total: 99.99, created_by_email: 'user@example.com' }  ← injecté par le serveur
```

Ces permissions se configurent dans le Dashboard → collection → schéma → colonne **SDK permission**.

---

## Storage

```js
// Lister les buckets du projet (X-Api-Key uniquement, pas de Bearer)
const buckets = await client.storage.listBuckets()
// → [{ id, name, visibility, public, created_at }, ...]

// Obtenir un client bucket
const bucket = client.storage.bucket('uuid-bucket')

// Lister les fichiers (paginé, filtre préfixe optionnel)
const { data, meta } = await bucket.listObjects({ page: 1, perPage: 20, prefix: 'users/123/' })
// → data: [{ id, name, content_type, size, created_by, created_at, url? }, ...]
// → meta: { total, page, per_page, pages }

// Upload un fichier (3 étapes gérées automatiquement : présignature → PUT OVH → confirmation)
// Bearer requis.
const file = document.querySelector('input[type=file]').files[0]
const obj = await bucket.upload(file, 'photo.jpg', {
  onProgress: pct => console.log(`${pct}%`), // progression optionnelle (navigateur uniquement)
})
// → { id, name, content_type, size, created_by, created_at, url? }

// Récupérer l'URL d'un fichier
const { url, expires_in } = await bucket.getUrl(obj.id)
// Bucket public  → url permanente, expires_in absent
// Bucket privé   → url signée (TTL 3600 s), créateur uniquement

// Supprimer un fichier (Bearer requis, créateur uniquement)
await bucket.delete(obj.id)
```

### Bucket privé — stocker et retrouver un fichier

Avec un bucket privé, l'URL signée expire après 1h. Il ne faut **pas** stocker l'URL — il faut stocker l'`id` de l'objet retourné par `upload()`, puis appeler `getUrl()` à chaque affichage.

**Workflow recommandé :**

```js
// 1. Upload → récupérer l'id de l'objet
const obj = await bucket.upload(file, 'photo.jpg')
const objectId = obj.id // ex: "a1b2c3d4-..."

// 2. Stocker l'id dans une collection FrenchBaas (pas l'URL)
await client.collection('profils').update(userId, {
  avatar_storage_id: objectId,
})

// 3. À l'affichage : récupérer l'id depuis la collection, puis générer l'URL
const profil = await client.collection('profils').getById(userId)
const storedId = profil.data.avatar_storage_id

const { url } = await bucket.getUrl(storedId)
// url → URL signée fraîche, valide 1h
```

> ⚠️ Ne stockez jamais l'URL signée — elle expire. Stockez toujours l'`id` de l'objet.

### Visibilité des buckets

| Visibilité | listObjects | upload / delete | getUrl |
|------------|-------------|-----------------|--------|
| `public` | Sans Bearer | Bearer requis | Sans Bearer (URL permanente) |
| `private` | Bearer requis (ses fichiers) | Bearer requis | Bearer requis (URL signée 1h) |

### Erreurs Storage

```ts
import { ConflictError, ValidationError, QuotaError } from '@frenchbaas/js'

try {
  await bucket.upload(file, 'photo.jpg')
} catch (e) {
  if (e instanceof ConflictError)   console.error('Nom déjà pris par un autre utilisateur')
  if (e instanceof ValidationError) console.error('Fichier trop grand ou type non autorisé')
  if (e instanceof QuotaError)      console.error('Quota storage dépassé')
}
```

---

## Webhooks

Si une collection a un `before_create_url` ou `before_update_url` configuré,
FrenchBaas appelle votre endpoint **avant chaque écriture en base**.
Votre serveur peut valider, enrichir ou rejeter les données.

Un rejet lève automatiquement une `ValidationError` côté SDK avec le message
renvoyé par votre webhook.

```js
import { ValidationError } from '@frenchbaas/js'

try {
  await client.collection('col-uuid').create({ montant: 150 })
} catch (e) {
  if (e instanceof ValidationError) {
    // e.message contient le message renvoyé par votre webhook
    // ex : "Stock insuffisant", "Utilisateur banni", etc.
    console.error('Rejeté par le webhook :', e.message)
  }
}

// Même comportement pour update()
try {
  await client.collection('col-uuid').update('doc-uuid', { montant: 200 })
} catch (e) {
  if (e instanceof ValidationError) {
    console.error('Mise à jour refusée :', e.message)
  }
}
```

> La configuration des webhooks (URL, secret HMAC) se fait dans le Dashboard
> ou via l'API REST. Voir la [documentation webhooks](https://frenchbaas.fr/documentation.html#webhooks).

---

## Gestion des erreurs

```ts
import {
  FrenchBaasError,
  AuthError,
  ConflictError,
  ValidationError,
  NotFoundError,
  QuotaError,
  RateLimitError,
  NetworkError,
  ServerError,
} from '@frenchbaas/js'

try {
  await client.collection('col-uuid').create({ title: 'Test' })
} catch (e) {
  if (e instanceof ValidationError) {
    // Données invalides (schéma) OU rejet par webhook
    // e.errors → string[] (erreurs de schéma)
    // e.message → message du webhook si rejet webhook (e.errors sera vide)
    console.error(e.message, e.errors)
  }
  else if (e instanceof AuthError)      console.error('Non connecté ou session expirée')
  else if (e instanceof ConflictError)  console.error('Conflit : ressource déjà existante')
  else if (e instanceof NotFoundError)  console.error('Document ou collection introuvable')
  else if (e instanceof QuotaError)     console.error('Quota dépassé :', e.message)
  else if (e instanceof RateLimitError) console.error('Trop de requêtes, réessayez dans quelques instants')
  else if (e instanceof NetworkError)   console.error('Impossible de joindre le serveur')
  else if (e instanceof ServerError)    console.error('Erreur interne du serveur')
  else if (e instanceof FrenchBaasError) console.error('Erreur FrenchBaas :', e.message)
}
```

Toutes les erreurs héritent de `FrenchBaasError` et exposent :
- `e.message` — description lisible
- `e.status` — code HTTP (401, 404, 422, 429, 5xx…)

`ValidationError` expose en plus :
- `e.errors` — tableau de messages de validation (vide si rejet webhook)

---

## Options

```js
const client = new FrenchBaas({
  url:     'https://app.frenchbaas.com',
  apiKey:  'votre-clé-api',
  storage: 'localStorage', // 'memory' (défaut) | 'localStorage'
})
```

| Option | Type | Défaut | Description |
|--------|------|--------|-------------|
| `url` | `string` | — | URL de votre instance FrenchBaas |
| `apiKey` | `string` | — | Clé API du projet (Dashboard → Projet) |
| `storage` | `'memory'` \| `'localStorage'` | `'memory'` | Persistance de la session |

Utilisez `localStorage` pour que la session survive au rechargement de page
(applications web). Utilisez `memory` pour les environnements serveur (Node.js).

> **Sécurité** : l'option `localStorage` expose les tokens à toute attaque XSS sur votre page.
> Assurez-vous que votre application ne charge pas de scripts tiers non maîtrisés
> et qu'elle applique une politique CSP stricte si vous utilisez ce mode.

---

## Fonctionnalités automatiques

- **Refresh token transparent** — access token rafraîchi automatiquement avant expiration
- **Retry sur 401** — relance la requête après refresh sans intervention du développeur
- **Déduplication** — plusieurs requêtes simultanées ne déclenchent qu'un seul refresh
- **Erreurs typées** — chaque erreur HTTP a sa classe TypeScript dédiée
- **Zero dépendance** — aucune dépendance runtime
- **TypeScript natif** — types `.d.ts` inclus, autocomplétion complète

---

## License

MIT