# Database

Ce répertoire est le **module de base de données**. Il fournit une couche d'accès PostgreSQL générique : connexion, requêtes paramétrées, transactions et helpers CRUD. Il ne connaît aucune feature spécifique — les features l'importent pour leurs propres besoins.

---

## Structure

```
src/core/database/
├── index.js    Exports publics (query, transaction, create, find, update…)
├── db.js       Pool de connexion, DatabaseError, fonctions de requête bas niveau
└── crud.js     Helpers CRUD (create, find, update, delete, count, exists…)
```

---

## Variables d'environnement

| Variable | Rôle |
|----------|------|
| `ZEN_DATABASE_URL` | URL de connexion PostgreSQL (tous environnements) |
| `ZEN_DATABASE_URL_DEV` | URL de connexion en développement (prioritaire sur `ZEN_DATABASE_URL` si `NODE_ENV=development`) |
| `ZEN_DB_SSL_DISABLED` | Mettre à `true` pour désactiver TLS entièrement (loopback local uniquement) |

### Politique TLS

| Environnement | Comportement |
|---------------|-------------|
| `production` | TLS activé, vérification du certificat serveur (`rejectUnauthorized: true`) |
| Autres (`development`, `test`…) | TLS activé, vérification désactivée (accepte les certificats auto-signés) |
| `ZEN_DB_SSL_DISABLED=true` | TLS désactivé (usage local uniquement) |

---

## DatabaseError

Toutes les opérations base de données lèvent une `DatabaseError` en cas d'échec. Elle expose uniquement un message générique et le code d'erreur PostgreSQL (SQLSTATE), sans jamais retourner de nom de table, de contrainte ou de fragment SQL.

```js
import { DatabaseError } from '@zen/core/database';

try {
  await create('users', { email });
} catch (error) {
  if (error instanceof DatabaseError && error.code === '23505') {
    // Violation de contrainte unique (ex: email déjà utilisé)
  }
}
```

Codes SQLSTATE courants :

| Code | Signification |
|------|--------------|
| `23505` | Violation de contrainte unique (`UNIQUE`) |
| `23503` | Violation de contrainte de clé étrangère (`FOREIGN KEY`) |
| `23502` | Violation de contrainte `NOT NULL` |

---

## Fonctions de requête bas niveau (`db.js`)

### `query(sql, params?)`

Exécute une requête SQL paramétrée et retourne l'objet résultat complet `pg`.

```js
const result = await query('SELECT * FROM users WHERE id = $1', [userId]);
// result.rows, result.rowCount…
```

### `queryOne(sql, params?)`

Retourne la première ligne ou `null`.

```js
const user = await queryOne('SELECT * FROM users WHERE email = $1', [email]);
```

### `queryAll(sql, params?)`

Retourne toutes les lignes sous forme de tableau.

```js
const posts = await queryAll('SELECT * FROM posts WHERE published = true');
```

### `transaction(callback)`

Exécute plusieurs requêtes dans une transaction atomique. Rollback automatique en cas d'erreur.

```js
const result = await transaction(async (client) => {
  const post = await client.query(
    'INSERT INTO posts (title) VALUES ($1) RETURNING *',
    ['Mon article']
  );
  await client.query(
    'INSERT INTO post_tags (post_id, tag) VALUES ($1, $2)',
    [post.rows[0].id, 'actu']
  );
  return post.rows[0];
});
```

### `testConnection()`

Vérifie que la connexion à la base est établie. Retourne `true` ou `false`.

### `tableExists(tableName)`

Vérifie si une table existe dans le schéma `public`. Retourne `true` ou `false`.

```js
if (!(await tableExists('users'))) {
  // table absente — migration non appliquée
}
```

---

## Helpers CRUD (`crud.js`)

Toutes les fonctions CRUD construisent du SQL **entièrement paramétré**. Les noms de tables et de colonnes passent par `safeIdentifier()` — injection SQL impossible.

### `create(tableName, data, options?)`

Insère un enregistrement et retourne la ligne créée (via `RETURNING *`).

```js
const user = await create('users', { email, name, role: 'user' });
```

Option `allowedColumns` (recommandée) : liste blanche explicite des colonnes autorisées. Toute clé absente de cette liste lève immédiatement une erreur.

```js
const user = await create('users', body, {
  allowedColumns: ['email', 'name'],
  // 'role' absent → une tentative de mass-assignment lèvera une erreur
});
```

### `findById(tableName, id, idColumn?)`

Retourne l'enregistrement correspondant à l'identifiant, ou `null`.

```js
const post = await findById('posts', 42);
const user = await findById('users', slug, 'slug');
```

### `find(tableName, conditions?, options?)`

Retourne un tableau d'enregistrements correspondant aux conditions.

```js
const published = await find('posts', { published: true }, {
  orderBy: 'created_at DESC',
  limit: 10,
  offset: 0,
});
```

Options disponibles :

| Option | Type | Contrainte |
|--------|------|-----------|
| `orderBy` | `string` | `"colonne"` ou `"colonne ASC\|DESC"` |
| `limit` | `number` | Entier entre 1 et 10 000 |
| `offset` | `number` | Entier ≥ 0 |

### `findOne(tableName, conditions)`

Retourne le premier enregistrement correspondant, ou `null`.

```js
const user = await findOne('users', { email });
```

### `updateById(tableName, id, data, idColumn?, options?)`

Met à jour un enregistrement par identifiant et retourne la ligne mise à jour, ou `null` si introuvable.

```js
const updated = await updateById('users', userId, { name: 'Alice' });

// Avec whitelist
const updated = await updateById('users', userId, body, 'id', {
  allowedColumns: ['name', 'avatar_url'],
});
```

### `update(tableName, conditions, data, options?)`

Met à jour tous les enregistrements correspondant aux conditions. **Au moins une condition est obligatoire** — une mise à jour sans condition lève immédiatement une erreur.

```js
const rows = await update('posts', { author_id: userId }, { published: false });
```

### `deleteById(tableName, id, idColumn?)`

Supprime un enregistrement par identifiant. Retourne `true` si supprimé, `false` sinon.

```js
const deleted = await deleteById('posts', postId);
```

### `deleteWhere(tableName, conditions)`

Supprime tous les enregistrements correspondant aux conditions. **Au moins une condition est obligatoire** — une suppression sans condition lève immédiatement une erreur. Retourne le nombre de lignes supprimées.

```js
const count = await deleteWhere('sessions', { user_id: userId });
```

### `count(tableName, conditions?)`

Retourne le nombre d'enregistrements correspondant aux conditions (ou le total si aucune condition).

```js
const total = await count('users');
const admins = await count('users', { role: 'admin' });
```

### `exists(tableName, conditions)`

Retourne `true` si au moins un enregistrement correspond aux conditions.

```js
const taken = await exists('users', { email });
```

---

## Utilitaires de sécurité

### `safeIdentifier(name)`

Valide et encadre de guillemets doubles un identifiant SQL (nom de table ou de colonne). Autorise uniquement `[A-Za-z_][A-Za-z0-9_]*`, longueur max 63. Lève une erreur pour tout identifiant invalide.

```js
safeIdentifier('users')    // → '"users"'
safeIdentifier('my-table') // → Error: SQL identifier contains disallowed characters
```

### `safeOrderBy(orderBy)`

Valide une expression `ORDER BY` : `"colonne"` ou `"colonne ASC|DESC"`. Utilise `safeIdentifier` en interne.

```js
safeOrderBy('created_at DESC') // → '"created_at" DESC'
safeOrderBy('1=1; DROP TABLE') // → Error
```

### `filterAllowedColumns(data, allowedColumns?)`

Filtre un objet de données selon une liste blanche de colonnes. Sans liste blanche, retourne l'objet tel quel. Avec liste blanche, toute clé absente lève immédiatement une erreur.

```js
filterAllowedColumns({ name: 'Alice', role: 'admin' }, ['name'])
// → Error: Column "role" is not in the permitted columns list
```

---

## Usage depuis une feature

```js
// src/features/myfeature/db.js
import { create, find, updateById, deleteById, exists } from '../../core/database/index.js';

export async function createItem(data) {
  return create('items', data, { allowedColumns: ['title', 'content', 'author_id'] });
}

export async function getPublishedItems() {
  return find('items', { published: true }, { orderBy: 'created_at DESC', limit: 50 });
}
```

## Usage depuis un module

```js
// src/modules/mymodule/crud.js
import { create, findById } from '@zen/core/database';

export async function createEntry(data) {
  return create('mymodule_entries', data, { allowedColumns: ['title', 'slug'] });
}
```
