outlet-orm 4.2.1 → 5.0.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
@@ -1,1212 +1,1312 @@
1
- # Outlet ORM
2
-
3
- [![npm version](https://badge.fury.io/js/outlet-orm.svg)](https://www.npmjs.com/package/outlet-orm)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
-
6
- Un ORM JavaScript inspiré de Laravel Eloquent pour Node.js avec support pour MySQL, PostgreSQL et SQLite.
7
-
8
- 📚 **[Documentation complète disponible dans `/docs`](./docs/INDEX.md)**
9
-
10
- ## ✅ Prérequis et compatibilité
11
-
12
- - Node.js >= 18 (recommandé/exigé)
13
- - Installez le driver de base de données correspondant à votre SGBD (voir ci-dessous)
14
-
15
- ## 🚀 Installation
16
-
17
- ```bash
18
- npm install outlet-orm
19
- ```
20
-
21
- ### Installer le driver de base de données
22
-
23
- Outlet ORM utilise des peerDependencies optionnelles pour les drivers de base de données. Installez uniquement le driver dont vous avez besoin:
24
-
25
- - MySQL/MariaDB: `npm install mysql2`
26
- - PostgreSQL: `npm install pg`
27
- - SQLite: `npm install sqlite3`
28
-
29
- Si aucun driver n'est installé, un message d'erreur explicite vous indiquera lequel installer lors de la connexion.
30
-
31
- ## 📁 Structure de Projet Recommandée
32
-
33
- Organisez votre projet utilisant Outlet ORM comme suit :
34
-
35
- > 🔐 **Sécurité** : Voir le [Guide de Sécurité](./docs/SECURITY.md) pour les bonnes pratiques.
36
-
37
- ```
38
- mon-projet/
39
- ├── .env # ⚠️ JAMAIS commité (dans .gitignore)
40
- ├── .env.example # Template sans secrets
41
- ├── .gitignore # Exclure .env, node_modules, logs
42
- ├── package.json
43
- ├── config/ # 🔒 Configuration centralisée
44
- ├── app.js # Config générale
45
- │ ├── database.js # Config DB (lit .env)
46
- └── security.js # Rate limit, helmet, CORS...
47
- ├── database/
48
- │ ├── config.js # Config migrations (généré par outlet-init)
49
- └── migrations/ # Vos fichiers de migration
50
- ├── models/ # Vos classes Model
51
- ├── User.js
52
- │ ├── Post.js
53
- └── Comment.js
54
- ├── controllers/ # Vos contrôleurs (logique métier)
55
- │ ├── UserController.js
56
- │ └── PostController.js
57
- ├── routes/ # Vos fichiers de routes
58
- │ ├── index.js
59
- └── userRoutes.js
60
- ├── middlewares/ # 🔒 Middlewares de sécurité
61
- ├── auth.js # Authentification JWT
62
- ├── authorization.js # Contrôle des permissions (RBAC)
63
- │ ├── rateLimiter.js # Protection anti-DDoS
64
- │ ├── validator.js # Validation des entrées
65
- └── errorHandler.js # Gestion centralisée des erreurs
66
- ├── services/ # Vos services (logique réutilisable)
67
- ├── AuthService.js
68
- └── EmailService.js
69
- ├── utils/ # 🔒 Utilitaires sécurité
70
- │ ├── hash.js # Hachage mots de passe (bcrypt)
71
- └── token.js # Génération tokens sécurisés
72
- ├── validators/ # 🔒 Schémas de validation
73
- └── userValidator.js
74
- ├── public/ # Seul dossier accessible publiquement
75
- │ ├── images/
76
- │ ├── css/
77
- └── js/
78
- ├── uploads/ # ⚠️ Fichiers uploadés (validés)
79
- ├── logs/ # 📋 Journaux (non versionnés)
80
- ├── src/ # Votre code applicatif
81
- └── index.js
82
- └── tests/ # Vos tests
83
- └── models.test.js
84
- ```
85
-
86
- | Dossier | Rôle | Sécurité |
87
- |---------|------|----------|
88
- | `config/` | Configuration centralisée | 🔒 Lit les secrets depuis .env |
89
- | `database/` | Migrations et config DB | `outlet-init` |
90
- | `models/` | Classes Model avec `hidden` et `fillable` | 🔒 Mass assignment protection |
91
- | `controllers/` | Logique métier | Valider les entrées |
92
- | `routes/` | Définition des routes API/Web | 🔒 Appliquer middlewares auth |
93
- | `middlewares/` | Auth, validation, rate limiting | 🔒 **Critique pour la sécurité** |
94
- | `services/` | Services réutilisables | Isoler la logique sensible |
95
- | `utils/` | Hash, tokens, encryption | 🔒 Ne jamais exposer |
96
- | `public/` | Seul dossier accessible | ✅ Fichiers statiques sûrs |
97
- | `logs/` | Journaux d'accès/erreurs | 📋 Dans .gitignore |
98
-
99
- ## Fonctionnalités clés
100
-
101
- - **API inspirée d'Eloquent** (Active Record) pour un usage fluide
102
- - **Query Builder expressif**: where/joins/order/limit/offset/paginate
103
- - **Filtres relationnels façon Laravel**: `whereHas()`, `has()`, `whereDoesntHave()`, `withCount()`
104
- - **Eager Loading** des relations via `.with(...)` avec contraintes et dot-notation
105
- - **Relations complètes**:
106
- - `hasOne`, `hasMany`, `belongsTo`, `belongsToMany` (avec attach/detach/sync)
107
- - `hasManyThrough`, `hasOneThrough` (relations transitives)
108
- - `morphOne`, `morphMany`, `morphTo` (relations polymorphiques)
109
- - **Transactions** complètes: `beginTransaction()`, `commit()`, `rollback()`, `transaction()`
110
- - **Soft Deletes**: suppression logique avec `deleted_at`, `withTrashed()`, `onlyTrashed()`, `restore()`
111
- - **Scopes**: globaux et locaux pour réutiliser vos filtres
112
- - **Events/Hooks**: `creating`, `created`, `updating`, `updated`, `deleting`, `deleted`, etc.
113
- - **Validation**: règles basiques intégrées (`required`, `email`, `min`, `max`, etc.)
114
- - **Query Logging**: mode debug avec `enableQueryLog()` et `getQueryLog()`
115
- - **Pool PostgreSQL**: connexions poolées pour de meilleures performances
116
- - **Protection SQL**: sanitization automatique des identifiants
117
- - **Casts automatiques** (int, float, boolean, json, date...)
118
- - **Attributs masqués** (`hidden`) et timestamps automatiques
119
- - **Contrôle de visibilité** des attributs cachés: `withHidden()` et `withoutHidden()`
120
- - **Incrément/Décrément atomiques**: `increment()` et `decrement()`
121
- - **Aliases ergonomiques**: `columns([...])`, `ordrer()` (alias typo de `orderBy`)
122
- - **Requêtes brutes**: `executeRawQuery()` et `execute()` (résultats natifs du driver)
123
- - **Migrations complètes** (create/alter/drop, index, foreign keys, batch tracking)
124
- - **CLI pratiques**: `outlet-init`, `outlet-migrate`, `outlet-convert`
125
- - **Configuration via `.env`** (chargée automatiquement)
126
- - **Multi-base de données**: MySQL, PostgreSQL et SQLite
127
- - **Types TypeScript complets** avec Generic Model et Schema Builder typé (v4.0.0+)
128
-
129
- ## ⚡ Démarrage Rapide
130
-
131
- ### Initialisation du projet
132
-
133
- ```bash
134
- # Créer la configuration initiale
135
- outlet-init
136
-
137
- # Créer une migration
138
- outlet-migrate make create_users_table
139
-
140
- # Exécuter les migrations
141
- outlet-migrate migrate
142
- ```
143
-
144
- ## 📖 Utilisation
145
-
146
- ### Configuration de la connexion
147
-
148
- Outlet ORM charge automatiquement la connexion depuis le fichier `.env`. **Plus besoin d'importer DatabaseConnection !**
149
-
150
- #### Fichier `.env`
151
-
152
- ```env
153
- DB_DRIVER=mysql
154
- DB_HOST=localhost
155
- DB_DATABASE=myapp
156
- DB_USER=root
157
- DB_PASSWORD=secret
158
- DB_PORT=3306
159
- ```
160
-
161
- #### Utilisation simplifiée
162
-
163
- ```javascript
164
- const { Model } = require('outlet-orm');
165
-
166
- class User extends Model {
167
- static table = 'users';
168
- }
169
-
170
- // C'est tout ! La connexion est automatique
171
- const users = await User.all();
172
- ```
173
-
174
- #### Configuration manuelle (optionnel)
175
-
176
- Si vous avez besoin de contrôler la connexion :
177
-
178
- ```javascript
179
- const { DatabaseConnection, Model } = require('outlet-orm');
180
-
181
- // Option 1 – via .env (aucun paramètre nécessaire)
182
- const db = new DatabaseConnection();
183
-
184
- // Option 2 – via objet de configuration
185
- const db = new DatabaseConnection({
186
- driver: 'mysql',
187
- host: 'localhost',
188
- database: 'myapp',
189
- user: 'root',
190
- password: 'secret',
191
- port: 3306
192
- });
193
-
194
- // Définir la connexion manuellement (optionnel)
195
- Model.setConnection(db);
196
- ```
197
-
198
- #### Variables d'environnement (.env)
199
-
200
- | Variable | Description | Par défaut |
201
- |----------|-------------|------------|
202
- | `DB_DRIVER` | `mysql`, `postgres`, `sqlite` | `mysql` |
203
- | `DB_HOST` | Hôte de la base | `localhost` |
204
- | `DB_PORT` | Port de connexion | Selon driver |
205
- | `DB_USER` / `DB_USERNAME` | Identifiant | - |
206
- | `DB_PASSWORD` | Mot de passe | - |
207
- | `DB_DATABASE` / `DB_NAME` | Nom de la base | - |
208
- | `DB_FILE` / `SQLITE_DB` | Fichier SQLite | `:memory:` |
209
-
210
- ### Importation
211
-
212
- ```javascript
213
- // CommonJS - Import simple (connexion automatique via .env)
214
- const { Model } = require('outlet-orm');
215
-
216
- // ES Modules
217
- import { Model } from 'outlet-orm';
218
-
219
- // Si besoin de contrôle manuel sur la connexion
220
- const { DatabaseConnection, Model } = require('outlet-orm');
221
- ```
222
-
223
- ### Définir un modèle
224
-
225
- ```javascript
226
- const { Model } = require('outlet-orm');
227
-
228
- // Définition des modèles liés (voir Relations)
229
- class Post extends Model { static table = 'posts'; }
230
- class Profile extends Model { static table = 'profiles'; }
231
-
232
- class User extends Model {
233
- static table = 'users';
234
- static primaryKey = 'id'; // Par défaut: 'id'
235
- static timestamps = true; // Par défaut: true
236
- static fillable = ['name', 'email', 'password'];
237
- static hidden = ['password'];
238
- static casts = {
239
- id: 'int',
240
- email_verified: 'boolean',
241
- metadata: 'json',
242
- birthday: 'date'
243
- };
244
-
245
- // Relations
246
- posts() {
247
- return this.hasMany(Post, 'user_id');
248
- }
249
-
250
- profile() {
251
- return this.hasOne(Profile, 'user_id');
252
- }
253
- }
254
- ```
255
-
256
- ### Opérations CRUD
257
-
258
- #### Créer
259
-
260
- ```javascript
261
- // Méthode 1: create()
262
- const user = await User.create({
263
- name: 'John Doe',
264
- email: 'john@example.com',
265
- password: 'secret123'
266
- });
267
-
268
- // Méthode 2: new + save()
269
- const user = new User({
270
- name: 'Jane Doe',
271
- email: 'jane@example.com'
272
- });
273
- user.setAttribute('password', 'secret456');
274
- await user.save();
275
-
276
- // Insert brut (sans créer d'instance)
277
- await User.insert({ name: 'Bob', email: 'bob@example.com' });
278
- ```
279
-
280
- #### Lire
281
-
282
- ```javascript
283
- // Tous les enregistrements
284
- const users = await User.all();
285
-
286
- // Par ID
287
- const user = await User.find(1);
288
- const user = await User.findOrFail(1); // Lance une erreur si non trouvé
289
-
290
- // Premier résultat
291
- const firstUser = await User.first();
292
-
293
- // Avec conditions
294
- const activeUsers = await User
295
- .where('status', 'active')
296
- .where('age', '>', 18)
297
- .get();
298
-
299
- // Avec relations (Eager Loading)
300
- const usersWithPosts = await User
301
- .with('posts', 'profile')
302
- .get();
303
-
304
- // Ordonner et limiter
305
- const recentUsers = await User
306
- .orderBy('created_at', 'desc')
307
- .limit(10)
308
- .get();
309
- ```
310
-
311
- #### Mettre à jour
312
-
313
- ```javascript
314
- // Instance
315
- const user = await User.find(1);
316
- user.setAttribute('name', 'Updated Name');
317
- await user.save();
318
-
319
- // Bulk update
320
- await User
321
- .where('status', 'pending')
322
- .update({ status: 'active' });
323
-
324
- // Update + Fetch (comme Prisma)
325
- const updated = await User
326
- .where('id', 1)
327
- .updateAndFetch({ name: 'Neo' }, ['profile', 'posts']);
328
-
329
- // Helpers par ID
330
- const user = await User.updateAndFetchById(1, { name: 'Trinity' }, ['profile']);
331
- await User.updateById(2, { status: 'active' });
332
- ```
333
-
334
- #### Supprimer
335
-
336
- ```javascript
337
- // Instance
338
- const user = await User.find(1);
339
- await user.destroy();
340
-
341
- // Bulk delete
342
- await User
343
- .where('status', 'banned')
344
- .delete();
345
- ```
346
-
347
- ### Query Builder
348
-
349
- ```javascript
350
- // Where clauses
351
- const users = await User
352
- .where('age', '>', 18)
353
- .where('status', 'active')
354
- .orWhere('role', 'admin')
355
- .get();
356
-
357
- // Where In / Not In
358
- const users = await User.whereIn('id', [1, 2, 3, 4, 5]).get();
359
- const users = await User.whereNotIn('status', ['banned', 'deleted']).get();
360
-
361
- // Where Null / Not Null
362
- const users = await User.whereNull('deleted_at').get();
363
- const verified = await User.whereNotNull('email_verified_at').get();
364
-
365
- // Where Between / Like
366
- const adults = await User.whereBetween('age', [18, 65]).get();
367
- const johns = await User.whereLike('name', '%john%').get();
368
-
369
- // Pagination
370
- const result = await User.paginate(1, 15);
371
- // { data: [...], total: 100, per_page: 15, current_page: 1, last_page: 7, from: 1, to: 15 }
372
-
373
- // Count / Exists
374
- const count = await User.where('status', 'active').count();
375
- const hasUsers = await User.where('role', 'admin').exists();
376
-
377
- // Joins
378
- const result = await User
379
- .join('profiles', 'users.id', 'profiles.user_id')
380
- .leftJoin('countries', 'profiles.country_id', 'countries.id')
381
- .select('users.*', 'profiles.bio', 'countries.name as country')
382
- .get();
383
-
384
- // Agrégations
385
- const stats = await User
386
- .distinct()
387
- .groupBy('status')
388
- .having('COUNT(*)', '>', 5)
389
- .get();
390
-
391
- // Incrément / Décrément atomique
392
- await User.where('id', 1).increment('login_count');
393
- await User.where('id', 1).decrement('credits', 10);
394
- ```
395
-
396
- ### Filtres relationnels
397
-
398
- ```javascript
399
- // whereHas: Utilisateurs ayant au moins un post publié
400
- const authors = await User
401
- .whereHas('posts', (q) => {
402
- q.where('status', 'published');
403
- })
404
- .get();
405
-
406
- // has: Au moins N enfants
407
- const prolific = await User.has('posts', '>=', 10).get();
408
-
409
- // whereDoesntHave: Aucun enfant
410
- const noPostUsers = await User.whereDoesntHave('posts').get();
411
-
412
- // withCount: Ajouter une colonne {relation}_count
413
- const withCounts = await User.withCount('posts').get();
414
- // Chaque user aura: user.getAttribute('posts_count')
415
- ```
416
-
417
- ## 🔗 Relations
418
-
419
- ### One to One (hasOne)
420
-
421
- ```javascript
422
- const { Model } = require('outlet-orm');
423
-
424
- class Profile extends Model { static table = 'profiles'; }
425
-
426
- class User extends Model {
427
- static table = 'users';
428
-
429
- profile() {
430
- return this.hasOne(Profile, 'user_id');
431
- }
432
- }
433
-
434
- const user = await User.find(1);
435
- const profile = await user.profile().get();
436
- ```
437
-
438
- ### One to Many (hasMany)
439
-
440
- ```javascript
441
- const { Model } = require('outlet-orm');
442
-
443
- class Post extends Model { static table = 'posts'; }
444
-
445
- class User extends Model {
446
- static table = 'users';
447
-
448
- posts() {
449
- return this.hasMany(Post, 'user_id');
450
- }
451
- }
452
-
453
- const user = await User.find(1);
454
- const posts = await user.posts().get();
455
- ```
456
-
457
- ### Belongs To (belongsTo)
458
-
459
- ```javascript
460
- const { Model } = require('outlet-orm');
461
-
462
- class User extends Model { static table = 'users'; }
463
-
464
- class Post extends Model {
465
- static table = 'posts';
466
-
467
- author() {
468
- return this.belongsTo(User, 'user_id');
469
- }
470
- }
471
-
472
- const post = await Post.find(1);
473
- const author = await post.author().get();
474
- ```
475
-
476
- ### Many to Many (belongsToMany)
477
-
478
- ```javascript
479
- const { Model } = require('outlet-orm');
480
-
481
- class Role extends Model { static table = 'roles'; }
482
-
483
- class User extends Model {
484
- static table = 'users';
485
-
486
- roles() {
487
- return this.belongsToMany(
488
- Role,
489
- 'user_roles', // Table pivot
490
- 'user_id', // FK vers User
491
- 'role_id' // FK vers Role
492
- );
493
- }
494
- }
495
-
496
- const user = await User.find(1);
497
- const roles = await user.roles().get();
498
-
499
- // Méthodes pivot
500
- await user.roles().attach([1, 2]); // Attacher des rôles
501
- await user.roles().detach(2); // Détacher un rôle
502
- await user.roles().sync([1, 3]); // Synchroniser (remplace tout)
503
- ```
504
-
505
- ### Has Many Through (hasManyThrough)
506
-
507
- Accéder à une relation distante via un modèle intermédiaire.
508
-
509
- ```javascript
510
- const { Model } = require('outlet-orm');
511
-
512
- class User extends Model {
513
- // User -> Post -> Comment
514
- comments() {
515
- return this.hasManyThrough(Comment, Post, 'user_id', 'post_id');
516
- }
517
- }
518
-
519
- const user = await User.find(1);
520
- const allComments = await user.comments().get();
521
- ```
522
-
523
- ### Has One Through (hasOneThrough)
524
-
525
- ```javascript
526
- const { Model } = require('outlet-orm');
527
-
528
- class User extends Model {
529
- // User -> Profile -> Country
530
- country() {
531
- return this.hasOneThrough(Country, Profile, 'user_id', 'country_id');
532
- }
533
- }
534
-
535
- const user = await User.find(1);
536
- const country = await user.country().get();
537
- ```
538
-
539
- ### Relations Polymorphiques
540
-
541
- Les relations polymorphiques permettent à un modèle d'appartenir à plusieurs autres modèles.
542
-
543
- ```javascript
544
- const { Model } = require('outlet-orm');
545
-
546
- // Configuration du morph map
547
- Model.setMorphMap({
548
- 'posts': Post,
549
- 'videos': Video
550
- });
551
-
552
- // Modèles
553
- class Post extends Model {
554
- comments() {
555
- return this.morphMany(Comment, 'commentable');
556
- }
557
- }
558
-
559
- class Video extends Model {
560
- comments() {
561
- return this.morphMany(Comment, 'commentable');
562
- }
563
- }
564
-
565
- class Comment extends Model {
566
- commentable() {
567
- return this.morphTo('commentable');
568
- }
569
- }
570
-
571
- // Usage
572
- const post = await Post.find(1);
573
- const comments = await post.comments().get();
574
-
575
- const comment = await Comment.find(1);
576
- const parent = await comment.commentable().get(); // Post ou Video
577
- ```
578
-
579
- **Relations polymorphiques disponibles:**
580
- - `morphOne(Related, 'morphName')` - One-to-One polymorphique
581
- - `morphMany(Related, 'morphName')` - One-to-Many polymorphique
582
- - `morphTo('morphName')` - Inverse polymorphique
583
-
584
- ### Eager Loading
585
-
586
- ```javascript
587
- // Charger plusieurs relations
588
- const users = await User.with('posts', 'profile', 'roles').get();
589
-
590
- // Charger avec contraintes
591
- const users = await User.with({
592
- posts: (q) => q.where('status', 'published').orderBy('created_at', 'desc')
593
- }).get();
594
-
595
- // Charger des relations imbriquées (dot notation)
596
- const users = await User.with('posts.comments.author').get();
597
-
598
- // Charger sur une instance existante
599
- const user = await User.find(1);
600
- await user.load('posts', 'profile');
601
- await user.load(['roles', 'posts.comments']);
602
-
603
- // Accéder aux relations chargées
604
- users.forEach(user => {
605
- console.log(user.relations.posts);
606
- console.log(user.relations.profile);
607
- });
608
- ```
609
-
610
- ## 🎭 Attributs
611
-
612
- ### Casts
613
-
614
- Les casts convertissent automatiquement les attributs:
615
-
616
- ```javascript
617
- const { Model } = require('outlet-orm');
618
-
619
- class User extends Model {
620
- static casts = {
621
- id: 'int', // ou 'integer'
622
- age: 'integer',
623
- balance: 'float', // ou 'double'
624
- email_verified: 'boolean', // ou 'bool'
625
- metadata: 'json', // Parse JSON
626
- settings: 'array', // Parse JSON en array
627
- birthday: 'date' // Convertit en Date
628
- };
629
- }
630
- ```
631
-
632
- ### Attributs cachés
633
-
634
- ```javascript
635
- const { Model } = require('outlet-orm');
636
-
637
- class User extends Model {
638
- static hidden = ['password', 'secret_token'];
639
- }
640
-
641
- const user = await User.find(1);
642
- console.log(user.toJSON()); // password et secret_token exclus
643
- ```
644
-
645
- #### Afficher les attributs cachés
646
-
647
- ```javascript
648
- // Inclure les attributs cachés
649
- const user = await User.withHidden().where('email', 'john@example.com').first();
650
- console.log(user.toJSON()); // password inclus
651
-
652
- // Contrôler avec un booléen
653
- const user = await User.withoutHidden(true).first(); // true = afficher
654
- const user = await User.withoutHidden(false).first(); // false = masquer (défaut)
655
-
656
- // Cas d'usage: authentification
657
- const user = await User.withHidden().where('email', email).first();
658
- if (user && await bcrypt.compare(password, user.getAttribute('password'))) {
659
- // Authentification réussie
660
- }
661
- ```
662
-
663
- ### Timestamps
664
-
665
- ```javascript
666
- const { Model } = require('outlet-orm');
667
-
668
- // Activés par défaut (created_at, updated_at)
669
- class User extends Model {
670
- static timestamps = true;
671
- }
672
-
673
- // Désactiver
674
- class Log extends Model {
675
- static timestamps = false;
676
- }
677
- ```
678
-
679
- ## 🔄 Transactions
680
-
681
- Outlet ORM supporte les transactions pour garantir l'intégrité des données:
682
-
683
- ```javascript
684
- const { DatabaseConnection, Model } = require('outlet-orm');
685
-
686
- // Méthode 1: Callback automatique (recommandé)
687
- const db = Model.connection;
688
- const result = await db.transaction(async (connection) => {
689
- const user = await User.create({ name: 'John', email: 'john@example.com' });
690
- await Account.create({ user_id: user.getAttribute('id'), balance: 0 });
691
- return user;
692
- });
693
- // Commit automatique, rollback si erreur
694
-
695
- // Méthode 2: Contrôle manuel
696
- await db.beginTransaction();
697
- try {
698
- await User.create({ name: 'Jane' });
699
- await db.commit();
700
- } catch (error) {
701
- await db.rollback();
702
- throw error;
703
- }
704
- ```
705
-
706
- ## 🗑️ Soft Deletes
707
-
708
- Suppression logique avec colonne `deleted_at`:
709
-
710
- ```javascript
711
- const { Model } = require('outlet-orm');
712
-
713
- class Post extends Model {
714
- static table = 'posts';
715
- static softDeletes = true;
716
- // static DELETED_AT = 'deleted_at'; // Personnalisable
717
- }
718
-
719
- // Les requêtes excluent automatiquement les supprimés
720
- const posts = await Post.all(); // Seulement les non-supprimés
721
-
722
- // Inclure les supprimés
723
- const allPosts = await Post.withTrashed().get();
724
-
725
- // Seulement les supprimés
726
- const trashedPosts = await Post.onlyTrashed().get();
727
-
728
- // Supprimer (soft delete)
729
- const post = await Post.find(1);
730
- await post.destroy(); // Met deleted_at à la date actuelle
731
-
732
- // Vérifier si supprimé
733
- if (post.trashed()) {
734
- console.log('Ce post est supprimé');
735
- }
736
-
737
- // Restaurer
738
- await post.restore();
739
-
740
- // Supprimer définitivement
741
- await post.forceDelete();
742
- ```
743
-
744
- ## 🔬 Scopes
745
-
746
- ### Scopes Globaux
747
-
748
- Appliqués automatiquement à toutes les requêtes:
749
-
750
- ```javascript
751
- const { Model } = require('outlet-orm');
752
-
753
- class Post extends Model {
754
- static table = 'posts';
755
- }
756
-
757
- // Ajouter un scope global
758
- Post.addGlobalScope('published', (query) => {
759
- query.where('status', 'published');
760
- });
761
-
762
- // Toutes les requêtes filtrent automatiquement
763
- const posts = await Post.all(); // Seulement les publiés
764
-
765
- // Désactiver temporairement un scope
766
- const allPosts = await Post.withoutGlobalScope('published').get();
767
-
768
- // Désactiver tous les scopes
769
- const rawPosts = await Post.withoutGlobalScopes().get();
770
- ```
771
-
772
- ## 📣 Events / Hooks
773
-
774
- Interceptez les opérations sur vos modèles:
775
-
776
- ```javascript
777
- const { Model } = require('outlet-orm');
778
-
779
- class User extends Model {
780
- static table = 'users';
781
- }
782
-
783
- // Avant création
784
- User.creating((user) => {
785
- user.setAttribute('uuid', generateUUID());
786
- // Retourner false pour annuler
787
- });
788
-
789
- // Après création
790
- User.created((user) => {
791
- console.log(`Utilisateur ${user.getAttribute('id')} créé`);
792
- });
793
-
794
- // Avant mise à jour
795
- User.updating((user) => {
796
- user.setAttribute('updated_at', new Date());
797
- });
798
-
799
- // Après mise à jour
800
- User.updated((user) => {
801
- // Notifier les systèmes externes
802
- });
803
-
804
- // Événements saving/saved (création ET mise à jour)
805
- User.saving((user) => {
806
- // Nettoyage des données
807
- });
808
-
809
- User.saved((user) => {
810
- // Cache invalidation
811
- });
812
-
813
- // Avant/après suppression
814
- User.deleting((user) => {
815
- // Vérifications avant suppression
816
- });
817
-
818
- User.deleted((user) => {
819
- // Nettoyage des relations
820
- });
821
-
822
- // Pour les soft deletes
823
- User.restoring((user) => {});
824
- User.restored((user) => {});
825
- ```
826
-
827
- ## ✅ Validation
828
-
829
- Validation basique intégrée:
830
-
831
- ```javascript
832
- const { Model } = require('outlet-orm');
833
-
834
- class User extends Model {
835
- static table = 'users';
836
- static rules = {
837
- name: 'required|string|min:2|max:100',
838
- email: 'required|email',
839
- age: 'numeric|min:0|max:150',
840
- role: 'in:admin,user,guest',
841
- password: 'required|min:8'
842
- };
843
- }
844
-
845
- const user = new User({
846
- name: 'J',
847
- email: 'invalid-email',
848
- age: 200
849
- });
850
-
851
- // Valider
852
- const { valid, errors } = user.validate();
853
- console.log(valid); // false
854
- console.log(errors);
855
- // {
856
- // name: ['name must be at least 2 characters'],
857
- // email: ['email must be a valid email'],
858
- // age: ['age must not exceed 150']
859
- // }
860
-
861
- // Valider ou lancer une erreur
862
- try {
863
- user.validateOrFail();
864
- } catch (error) {
865
- console.log(error.errors);
866
- }
867
- ```
868
-
869
- ### Règles disponibles
870
-
871
- | Règle | Description |
872
- |-------|-------------|
873
- | `required` | Champ obligatoire |
874
- | `string` | Doit être une chaîne |
875
- | `number` / `numeric` | Doit être un nombre |
876
- | `email` | Format email valide |
877
- | `boolean` | Doit être un booléen |
878
- | `date` | Date valide |
879
- | `min:N` | Minimum N (longueur ou valeur) |
880
- | `max:N` | Maximum N (longueur ou valeur) |
881
- | `in:a,b,c` | Valeur parmi la liste |
882
- | `regex:pattern` | Match le pattern regex |
883
-
884
- ## 📊 Query Logging
885
-
886
- Mode debug pour analyser vos requêtes:
887
-
888
- ```javascript
889
- const { Model } = require('outlet-orm');
890
-
891
- // Activer le logging
892
- const db = Model.getConnection();
893
- db.enableQueryLog();
894
-
895
- // Exécuter des requêtes
896
- await User.where('status', 'active').get();
897
- await Post.with('author').get();
898
-
899
- // Récupérer le log
900
- const queries = db.getQueryLog();
901
- console.log(queries);
902
- // [
903
- // { sql: 'SELECT * FROM users WHERE status = ?', params: ['active'], duration: 15, timestamp: Date },
904
- // { sql: 'SELECT * FROM posts', params: [], duration: 8, timestamp: Date }
905
- // ]
906
-
907
- // Vider le log
908
- db.flushQueryLog();
909
-
910
- // Désactiver le logging
911
- db.disableQueryLog();
912
-
913
- // Vérifier si actif
914
- if (db.isLogging()) {
915
- console.log('Logging actif');
916
- }
917
- ```
918
-
919
- ## 📝 API Reference
920
-
921
- ### DatabaseConnection
922
-
923
- | Méthode | Description |
924
- |---------|-------------|
925
- | `new DatabaseConnection(config?)` | Crée une connexion (lit `.env` si config omis) |
926
- | `connect()` | Établit la connexion (appelé automatiquement) |
927
- | `beginTransaction()` | Démarre une transaction |
928
- | `commit()` | Valide la transaction |
929
- | `rollback()` | Annule la transaction |
930
- | `transaction(callback)` | Exécute dans une transaction (auto commit/rollback) |
931
- | `select(table, query)` | Exécute un SELECT |
932
- | `insert(table, data)` | Insère un enregistrement |
933
- | `insertMany(table, data[])` | Insère plusieurs enregistrements |
934
- | `update(table, data, query)` | Met à jour des enregistrements |
935
- | `delete(table, query)` | Supprime des enregistrements |
936
- | `count(table, query)` | Compte les enregistrements |
937
- | `executeRawQuery(sql, params?)` | Requête brute (résultats normalisés) |
938
- | `execute(sql, params?)` | Requête brute (résultats natifs driver) |
939
- | `increment(table, column, query, amount?)` | Incrément atomique |
940
- | `decrement(table, column, query, amount?)` | Décrément atomique |
941
- | `close()` / `disconnect()` | Ferme la connexion |
942
- | **Query Logging (static)** | |
943
- | `enableQueryLog()` | Active le logging des requêtes |
944
- | `disableQueryLog()` | Désactive le logging |
945
- | `getQueryLog()` | Retourne le log des requêtes |
946
- | `flushQueryLog()` | Vide le log |
947
- | `isLogging()` | Vérifie si le logging est actif |
948
-
949
- ### Model (méthodes statiques)
950
-
951
- | Méthode | Description |
952
- |---------|-------------|
953
- | `setConnection(db)` | Définit la connexion par défaut |
954
- | `getConnection()` | Récupère la connexion (v3.0.0+) |
955
- | `setMorphMap(map)` | Définit le mapping polymorphique |
956
- | `query()` | Retourne un QueryBuilder |
957
- | `all()` | Tous les enregistrements |
958
- | `find(id)` | Trouve par ID |
959
- | `findOrFail(id)` | Trouve ou lance une erreur |
960
- | `first()` | Premier enregistrement |
961
- | `where(col, op?, val)` | Clause WHERE |
962
- | `whereIn(col, vals)` | Clause WHERE IN |
963
- | `whereNull(col)` | Clause WHERE NULL |
964
- | `whereNotNull(col)` | Clause WHERE NOT NULL |
965
- | `create(attrs)` | Crée et sauvegarde |
966
- | `insert(data)` | Insert brut |
967
- | `update(attrs)` | Update bulk |
968
- | `updateById(id, attrs)` | Update par ID |
969
- | `updateAndFetchById(id, attrs, rels?)` | Update + fetch avec relations |
970
- | `delete()` | Delete bulk |
971
- | `with(...rels)` | Eager loading |
972
- | `withHidden()` | Inclut les attributs cachés |
973
- | `withoutHidden(show?)` | Contrôle visibilité |
974
- | `orderBy(col, dir?)` | Tri |
975
- | `limit(n)` / `offset(n)` | Limite/Offset |
976
- | `paginate(page, perPage)` | Pagination |
977
- | `count()` | Compte |
978
- | **Soft Deletes** | |
979
- | `withTrashed()` | Inclut les supprimés |
980
- | `onlyTrashed()` | Seulement les supprimés |
981
- | **Scopes** | |
982
- | `addGlobalScope(name, cb)` | Ajoute un scope global |
983
- | `removeGlobalScope(name)` | Supprime un scope |
984
- | `withoutGlobalScope(name)` | Requête sans un scope |
985
- | `withoutGlobalScopes()` | Requête sans tous les scopes |
986
- | **Events** | |
987
- | `on(event, callback)` | Enregistre un listener |
988
- | `creating(cb)` / `created(cb)` | Events création |
989
- | `updating(cb)` / `updated(cb)` | Events mise à jour |
990
- | `saving(cb)` / `saved(cb)` | Events sauvegarde |
991
- | `deleting(cb)` / `deleted(cb)` | Events suppression |
992
- | `restoring(cb)` / `restored(cb)` | Events restauration |
993
-
994
- ### Model (méthodes d'instance)
995
-
996
- | Méthode | Description |
997
- |---------|-------------|
998
- | `fill(attrs)` | Remplit les attributs |
999
- | `setAttribute(key, val)` | Définit un attribut |
1000
- | `getAttribute(key)` | Récupère un attribut |
1001
- | `save()` | Sauvegarde (insert ou update) |
1002
- | `destroy()` | Supprime l'instance (soft si activé) |
1003
- | `load(...rels)` | Charge des relations |
1004
- | `getDirty()` | Attributs modifiés |
1005
- | `isDirty()` | A été modifié? |
1006
- | `toJSON()` | Convertit en objet |
1007
- | **Soft Deletes** | |
1008
- | `trashed()` | Est supprimé? |
1009
- | `restore()` | Restaure le modèle |
1010
- | `forceDelete()` | Suppression définitive |
1011
- | **Validation** | |
1012
- | `validate()` | Valide selon les règles |
1013
- | `validateOrFail()` | Valide ou lance erreur |
1014
-
1015
- ### QueryBuilder
1016
-
1017
- | Méthode | Description |
1018
- |---------|-------------|
1019
- | `select(...cols)` / `columns([...])` | Sélection de colonnes |
1020
- | `distinct()` | SELECT DISTINCT |
1021
- | `where(col, op?, val)` | Clause WHERE |
1022
- | `whereIn(col, vals)` | WHERE IN |
1023
- | `whereNotIn(col, vals)` | WHERE NOT IN |
1024
- | `whereNull(col)` | WHERE NULL |
1025
- | `whereNotNull(col)` | WHERE NOT NULL |
1026
- | `orWhere(col, op?, val)` | OR WHERE |
1027
- | `whereBetween(col, [min, max])` | WHERE BETWEEN |
1028
- | `whereLike(col, pattern)` | WHERE LIKE |
1029
- | `whereHas(rel, cb?)` | Filtre par relation |
1030
- | `has(rel, op?, count)` | Existence relationnelle |
1031
- | `whereDoesntHave(rel)` | Absence de relation |
1032
- | `orderBy(col, dir?)` / `ordrer(...)` | Tri |
1033
- | `limit(n)` / `take(n)` | Limite |
1034
- | `offset(n)` / `skip(n)` | Offset |
1035
- | `groupBy(...cols)` | GROUP BY |
1036
- | `having(col, op, val)` | HAVING |
1037
- | `join(table, first, op?, second)` | INNER JOIN |
1038
- | `leftJoin(table, first, op?, second)` | LEFT JOIN |
1039
- | `with(...rels)` | Eager loading |
1040
- | `withCount(rels)` | Ajoute {rel}_count |
1041
- | `withTrashed()` | Inclut les supprimés |
1042
- | `onlyTrashed()` | Seulement les supprimés |
1043
- | `withoutGlobalScope(name)` | Sans un scope global |
1044
- | `withoutGlobalScopes()` | Sans tous les scopes |
1045
- | `get()` | Exécute et retourne tous |
1046
- | `first()` | Premier résultat |
1047
- | `firstOrFail()` | Premier ou erreur |
1048
- | `paginate(page, perPage)` | Pagination |
1049
- | `count()` | Compte |
1050
- | `exists()` | Vérifie l'existence |
1051
- | `insert(data)` | Insert |
1052
- | `update(attrs)` | Update |
1053
- | `updateAndFetch(attrs, rels?)` | Update + fetch |
1054
- | `delete()` | Delete |
1055
- | `increment(col, amount?)` | Incrément atomique |
1056
- | `decrement(col, amount?)` | Décrément atomique |
1057
- | `clone()` | Clone le query builder |
1058
-
1059
- ## 🛠️ Outils CLI
1060
-
1061
- ### outlet-init
1062
-
1063
- Initialise un nouveau projet avec configuration de base de données.
1064
-
1065
- ```bash
1066
- outlet-init
1067
- ```
1068
-
1069
- Génère:
1070
- - Fichier de configuration `database/config.js`
1071
- - Fichier `.env` avec les paramètres
1072
- - Modèle exemple
1073
- - Fichier d'utilisation
1074
-
1075
- ### outlet-migrate
1076
-
1077
- Système complet de migrations.
1078
-
1079
- ```bash
1080
- # Créer une migration
1081
- outlet-migrate make create_users_table
1082
-
1083
- # Exécuter les migrations
1084
- outlet-migrate migrate
1085
-
1086
- # Voir le statut
1087
- outlet-migrate status
1088
-
1089
- # Annuler la dernière migration
1090
- outlet-migrate rollback --steps 1
1091
-
1092
- # Reset toutes les migrations
1093
- outlet-migrate reset --yes
1094
-
1095
- # Refresh (reset + migrate)
1096
- outlet-migrate refresh --yes
1097
-
1098
- # Fresh (drop all + migrate)
1099
- outlet-migrate fresh --yes
1100
- ```
1101
-
1102
- **Fonctionnalités des Migrations:**
1103
-
1104
- - Création et gestion des migrations (create, alter, drop tables)
1105
- - Types de colonnes: id, string, text, integer, boolean, date, datetime, timestamp, decimal, float, json, enum, uuid, foreignId
1106
- - Modificateurs: nullable, default, unique, index, unsigned, autoIncrement, comment, after, first
1107
- - Clés étrangères: foreign(), constrained(), onDelete(), onUpdate(), CASCADE
1108
- - ✅ Index: index(), unique(), fullText()
1109
- - ✅ Manipulation: renameColumn(), dropColumn(), dropTimestamps()
1110
- - Migrations réversibles: Méthodes up() et down()
1111
- - Batch tracking: Rollback précis par batch
1112
- - SQL personnalisé: execute() pour commandes avancées
1113
-
1114
- ### outlet-convert
1115
-
1116
- Convertit des schémas SQL en modèles ORM.
1117
-
1118
- ```bash
1119
- outlet-convert
1120
- ```
1121
-
1122
- **Options:**
1123
- 1. Depuis un fichier SQL local
1124
- 2. Depuis une base de données connectée
1125
-
1126
- **Fonctionnalités:**
1127
- - Détection automatique des types et casts
1128
- - Génération automatique de TOUTES les relations (belongsTo, hasMany, hasOne, belongsToMany)
1129
- - Relations récursives (auto-relations)
1130
- - Détection des champs sensibles (password, token, etc.)
1131
- - Support des timestamps automatiques
1132
- - Conversion des noms en PascalCase
1133
-
1134
- ## 📚 Documentation
1135
-
1136
- - [Guide des Migrations](docs/MIGRATIONS.md)
1137
- - [Conversion SQL](docs/SQL_CONVERSION.md)
1138
- - [Détection des Relations](docs/RELATIONS_DETECTION.md)
1139
- - [Guide de démarrage rapide](docs/QUICKSTART.md)
1140
- - [Architecture](docs/ARCHITECTURE.md)
1141
- - [**TypeScript (complet)**](docs/TYPESCRIPT.md)
1142
-
1143
- ## 📘 TypeScript Support
1144
-
1145
- Outlet ORM v4.0.0 inclut des définitions TypeScript complètes avec support des **generics pour les attributs typés**.
1146
-
1147
- ### Modèles typés
1148
-
1149
- ```typescript
1150
- import { Model, HasManyRelation } from 'outlet-orm';
1151
-
1152
- interface UserAttributes {
1153
- id: number;
1154
- name: string;
1155
- email: string;
1156
- role: 'admin' | 'user';
1157
- created_at: Date;
1158
- }
1159
-
1160
- class User extends Model<UserAttributes> {
1161
- static table = 'users';
1162
- static fillable = ['name', 'email', 'role'];
1163
-
1164
- posts(): HasManyRelation<Post> {
1165
- return this.hasMany(Post, 'user_id');
1166
- }
1167
- }
1168
-
1169
- // Type-safe getAttribute/setAttribute
1170
- const user = await User.find(1);
1171
- const name: string = user.getAttribute('name'); // ✅ Type inféré
1172
- const role: 'admin' | 'user' = user.getAttribute('role');
1173
- ```
1174
-
1175
- ### Migrations typées
1176
-
1177
- ```typescript
1178
- import { MigrationInterface, Schema, TableBuilder } from 'outlet-orm';
1179
-
1180
- export const migration: MigrationInterface = {
1181
- name: 'create_users_table',
1182
-
1183
- async up(): Promise<void> {
1184
- await Schema.create('users', (table: TableBuilder) => {
1185
- table.id();
1186
- table.string('name');
1187
- table.string('email').unique();
1188
- table.timestamps();
1189
- });
1190
- },
1191
-
1192
- async down(): Promise<void> {
1193
- await Schema.dropIfExists('users');
1194
- }
1195
- };
1196
- ```
1197
-
1198
- 📖 [Guide TypeScript complet](docs/TYPESCRIPT.md)
1199
-
1200
- ## 🤝 Contribution
1201
-
1202
- Les contributions sont les bienvenues! N'hésitez pas à ouvrir une issue ou un pull request.
1203
-
1204
- Voir [CONTRIBUTING.md](CONTRIBUTING.md) pour les guidelines.
1205
-
1206
- ## 📄 Licence
1207
-
1208
- MIT - Voir [LICENSE](LICENSE) pour plus de détails.
1209
-
1210
- ---
1211
-
1212
- Créé par [omgbwa-yasse](https://github.com/omgbwa-yasse)
1
+ # Outlet ORM
2
+
3
+ [![npm version](https://badge.fury.io/js/outlet-orm.svg)](https://www.npmjs.com/package/outlet-orm)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Un ORM JavaScript inspiré de Laravel Eloquent pour Node.js avec support pour MySQL, PostgreSQL et SQLite.
7
+
8
+ 📚 **[Documentation complète disponible dans `/docs`](./docs/INDEX.md)**
9
+
10
+ ## ✅ Prérequis et compatibilité
11
+
12
+ - Node.js >= 18 (recommandé/exigé)
13
+ - Installez le driver de base de données correspondant à votre SGBD (voir ci-dessous)
14
+
15
+ ## 🚀 Installation
16
+
17
+ ```bash
18
+ npm install outlet-orm
19
+ ```
20
+
21
+ ### Installer le driver de base de données
22
+
23
+ Outlet ORM utilise des peerDependencies optionnelles pour les drivers de base de données. Installez uniquement le driver dont vous avez besoin:
24
+
25
+ - MySQL/MariaDB: `npm install mysql2`
26
+ - PostgreSQL: `npm install pg`
27
+ - SQLite: `npm install sqlite3`
28
+
29
+ Si aucun driver n'est installé, un message d'erreur explicite vous indiquera lequel installer lors de la connexion.
30
+
31
+ ## 📁 Structure de Projet Recommandée
32
+
33
+ Organisez votre projet utilisant Outlet ORM avec une **architecture en couches** (recommandée pour la production) :
34
+
35
+ > 🔐 **Sécurité** : Voir le [Guide de Sécurité](./docs/SECURITY.md) pour les bonnes pratiques.
36
+
37
+ ```
38
+ mon-projet/
39
+ ├── .env # ⚠️ JAMAIS commité (dans .gitignore)
40
+ ├── .env.example # Template sans secrets
41
+ ├── .gitignore
42
+ ├── package.json
43
+
44
+ ├── src/ # 📦 Code source centralisé
45
+ │ ├── index.js # Point d'entrée de l'application
46
+
47
+ ├── config/ # ⚙️ Configuration
48
+ ├── app.js # Config générale (port, env)
49
+ │ ├── database.js # Config DB (lit .env)
50
+ │ │ └── security.js # CORS, helmet, rate limit
51
+
52
+ │ ├── models/ # 📊 Couche Data (Entities)
53
+ │ ├── index.js # Export centralisé des models
54
+ │ │ ├── User.js
55
+ ├── Post.js
56
+ └── Comment.js
57
+ │ │
58
+ │ ├── repositories/ # 🗄️ Couche Accès Données
59
+ │ ├── BaseRepository.js # Méthodes CRUD génériques
60
+ │ │ ├── UserRepository.js # Requêtes spécifiques User
61
+ │ └── PostRepository.js
62
+
63
+ │ ├── services/ # 💼 Couche Métier (Business Logic)
64
+ ├── AuthService.js # Logique d'authentification
65
+ │ ├── UserService.js # Logique métier utilisateur
66
+ │ │ ├── PostService.js
67
+ │ └── EmailService.js # Service externe (emails)
68
+
69
+ ├── controllers/ # 🎮 Couche Présentation (HTTP)
70
+ ├── AuthController.js
71
+ │ ├── UserController.js
72
+ │ │ └── PostController.js
73
+
74
+ ├── routes/ # 🛤️ Définition des routes
75
+ ├── index.js # Agrégateur de routes
76
+ ├── auth.routes.js
77
+ │ ├── user.routes.js
78
+ │ │ └── post.routes.js
79
+ │ │
80
+ ├── middlewares/ # 🔒 Middlewares
81
+ │ ├── auth.js # JWT verification
82
+ │ │ ├── authorize.js # RBAC / permissions
83
+ │ │ ├── rateLimiter.js # Protection DDoS
84
+ │ │ ├── validator.js # Validation request body
85
+ │ │ └── errorHandler.js # Gestion centralisée erreurs
86
+ │ │
87
+ │ ├── validators/ # ✅ Schémas de validation
88
+ │ │ ├── authValidator.js
89
+ │ │ └── userValidator.js
90
+ │ │
91
+ │ └── utils/ # 🔧 Utilitaires
92
+ │ ├── hash.js # bcrypt wrapper
93
+ │ ├── token.js # JWT helpers
94
+ │ ├── logger.js # Winston/Pino config
95
+ │ └── response.js # Formatage réponses API
96
+
97
+ ├── database/
98
+ │ ├── config.js # Config migrations (outlet-init)
99
+ │ ├── migrations/ # Fichiers de migration
100
+ │ └── seeders/ # Données de test/démo
101
+ │ └── UserSeeder.js
102
+
103
+ ├── public/ # Fichiers statiques publics
104
+ │ ├── images/
105
+ │ ├── css/
106
+ │ └── js/
107
+
108
+ ├── uploads/ # ⚠️ Fichiers uploadés
109
+
110
+ ├── logs/ # 📋 Journaux (non versionnés)
111
+
112
+ └── tests/ # 🧪 Tests
113
+ ├── unit/ # Tests unitaires
114
+ │ ├── services/
115
+ │ └── models/
116
+ ├── integration/ # Tests d'intégration
117
+ │ └── api/
118
+ └── fixtures/ # Données de test
119
+ └── users.json
120
+ ```
121
+
122
+ ### 🏗️ Architecture en Couches
123
+
124
+ ```
125
+ ┌─────────────────────────────────────────────────────────────┐
126
+ │ HTTP Request │
127
+ └─────────────────────────────────────────────────────────────┘
128
+
129
+
130
+ ┌─────────────────────────────────────────────────────────────┐
131
+ │ MIDDLEWARES: auth validate → rateLimiter → errorHandler │
132
+ └─────────────────────────────────────────────────────────────┘
133
+
134
+
135
+ ┌─────────────────────────────────────────────────────────────┐
136
+ │ ROUTES → CONTROLLERS (Couche Présentation) │
137
+ │ Reçoit la requête, appelle le service, retourne réponse │
138
+ └─────────────────────────────────────────────────────────────┘
139
+
140
+
141
+ ┌─────────────────────────────────────────────────────────────┐
142
+ │ SERVICES (Couche Métier / Business Logic) │
143
+ │ Logique métier, orchestration, règles business │
144
+ └─────────────────────────────────────────────────────────────┘
145
+
146
+
147
+ ┌─────────────────────────────────────────────────────────────┐
148
+ │ REPOSITORIES (Couche Accès Données) │
149
+ │ Abstraction des requêtes DB, utilise les Models │
150
+ └─────────────────────────────────────────────────────────────┘
151
+
152
+
153
+ ┌─────────────────────────────────────────────────────────────┐
154
+ │ MODELS (Outlet ORM) → DATABASE │
155
+ └─────────────────────────────────────────────────────────────┘
156
+ ```
157
+
158
+ ### 📋 Rôle de chaque couche
159
+
160
+ | Couche | Dossier | Responsabilité | Dépend de |
161
+ |--------|---------|----------------|-----------|
162
+ | **Présentation** | `controllers/` | Traiter HTTP, valider entrées, formater réponses | Services |
163
+ | **Métier** | `services/` | Logique business, orchestration, règles | Repositories |
164
+ | **Données** | `repositories/` | Requêtes DB complexes, abstraction | Models |
165
+ | **Entités** | `models/` | Définition des entités, relations, validations | Outlet ORM |
166
+
167
+ ### Avantages de cette architecture
168
+
169
+ - **Testabilité** : Chaque couche peut être testée indépendamment
170
+ - **Maintenabilité** : Séparation claire des responsabilités
171
+ - **Scalabilité** : Facile d'ajouter de nouvelles fonctionnalités
172
+ - **Réutilisabilité** : Services utilisables depuis CLI, workers, etc.
173
+
174
+ ### 📝 Exemple de flux
175
+
176
+ ```javascript
177
+ // routes/user.routes.js
178
+ router.get('/users/:id', auth, UserController.show);
179
+
180
+ // controllers/UserController.js
181
+ async show(req, res) {
182
+ const user = await userService.findById(req.params.id);
183
+ res.json({ data: user });
184
+ }
185
+
186
+ // services/UserService.js
187
+ async findById(id) {
188
+ const user = await userRepository.findWithPosts(id);
189
+ if (!user) throw new NotFoundError('User not found');
190
+ return user;
191
+ }
192
+
193
+ // repositories/UserRepository.js
194
+ async findWithPosts(id) {
195
+ return User.with('posts').find(id);
196
+ }
197
+ ```
198
+
199
+ ## ✨ Fonctionnalités clés
200
+
201
+ - **API inspirée d'Eloquent** (Active Record) pour un usage fluide
202
+ - **Query Builder expressif**: where/joins/order/limit/offset/paginate
203
+ - **Filtres relationnels façon Laravel**: `whereHas()`, `has()`, `whereDoesntHave()`, `withCount()`
204
+ - **Eager Loading** des relations via `.with(...)` avec contraintes et dot-notation
205
+ - **Relations complètes**:
206
+ - `hasOne`, `hasMany`, `belongsTo`, `belongsToMany` (avec attach/detach/sync)
207
+ - `hasManyThrough`, `hasOneThrough` (relations transitives)
208
+ - `morphOne`, `morphMany`, `morphTo` (relations polymorphiques)
209
+ - **Transactions** complètes: `beginTransaction()`, `commit()`, `rollback()`, `transaction()`
210
+ - **Soft Deletes**: suppression logique avec `deleted_at`, `withTrashed()`, `onlyTrashed()`, `restore()`
211
+ - **Scopes**: globaux et locaux pour réutiliser vos filtres
212
+ - **Events/Hooks**: `creating`, `created`, `updating`, `updated`, `deleting`, `deleted`, etc.
213
+ - **Validation**: règles basiques intégrées (`required`, `email`, `min`, `max`, etc.)
214
+ - **Query Logging**: mode debug avec `enableQueryLog()` et `getQueryLog()`
215
+ - **Pool PostgreSQL**: connexions poolées pour de meilleures performances
216
+ - **Protection SQL**: sanitization automatique des identifiants
217
+ - **Casts automatiques** (int, float, boolean, json, date...)
218
+ - **Attributs masqués** (`hidden`) et timestamps automatiques
219
+ - **Contrôle de visibilité** des attributs cachés: `withHidden()` et `withoutHidden()`
220
+ - **Incrément/Décrément atomiques**: `increment()` et `decrement()`
221
+ - **Aliases ergonomiques**: `columns([...])`, `ordrer()` (alias typo de `orderBy`)
222
+ - **Requêtes brutes**: `executeRawQuery()` et `execute()` (résultats natifs du driver)
223
+ - **Migrations complètes** (create/alter/drop, index, foreign keys, batch tracking)
224
+ - **CLI pratiques**: `outlet-init`, `outlet-migrate`, `outlet-convert`
225
+ - **Configuration via `.env`** (chargée automatiquement)
226
+ - **Multi-base de données**: MySQL, PostgreSQL et SQLite
227
+ - **Types TypeScript complets** avec Generic Model et Schema Builder typé (v4.0.0+)
228
+
229
+ ## Démarrage Rapide
230
+
231
+ ### Initialisation du projet
232
+
233
+ ```bash
234
+ # Créer la configuration initiale
235
+ outlet-init
236
+
237
+ # Créer une migration
238
+ outlet-migrate make create_users_table
239
+
240
+ # Exécuter les migrations
241
+ outlet-migrate migrate
242
+ ```
243
+
244
+ ## 📖 Utilisation
245
+
246
+ ### Configuration de la connexion
247
+
248
+ Outlet ORM charge automatiquement la connexion depuis le fichier `.env`. **Plus besoin d'importer DatabaseConnection !**
249
+
250
+ #### Fichier `.env`
251
+
252
+ ```env
253
+ DB_DRIVER=mysql
254
+ DB_HOST=localhost
255
+ DB_DATABASE=myapp
256
+ DB_USER=root
257
+ DB_PASSWORD=secret
258
+ DB_PORT=3306
259
+ ```
260
+
261
+ #### Utilisation simplifiée
262
+
263
+ ```javascript
264
+ const { Model } = require('outlet-orm');
265
+
266
+ class User extends Model {
267
+ static table = 'users';
268
+ }
269
+
270
+ // C'est tout ! La connexion est automatique
271
+ const users = await User.all();
272
+ ```
273
+
274
+ #### Configuration manuelle (optionnel)
275
+
276
+ Si vous avez besoin de contrôler la connexion :
277
+
278
+ ```javascript
279
+ const { DatabaseConnection, Model } = require('outlet-orm');
280
+
281
+ // Option 1 – via .env (aucun paramètre nécessaire)
282
+ const db = new DatabaseConnection();
283
+
284
+ // Option 2 via objet de configuration
285
+ const db = new DatabaseConnection({
286
+ driver: 'mysql',
287
+ host: 'localhost',
288
+ database: 'myapp',
289
+ user: 'root',
290
+ password: 'secret',
291
+ port: 3306
292
+ });
293
+
294
+ // Définir la connexion manuellement (optionnel)
295
+ Model.setConnection(db);
296
+ ```
297
+
298
+ #### Variables d'environnement (.env)
299
+
300
+ | Variable | Description | Par défaut |
301
+ |----------|-------------|------------|
302
+ | `DB_DRIVER` | `mysql`, `postgres`, `sqlite` | `mysql` |
303
+ | `DB_HOST` | Hôte de la base | `localhost` |
304
+ | `DB_PORT` | Port de connexion | Selon driver |
305
+ | `DB_USER` / `DB_USERNAME` | Identifiant | - |
306
+ | `DB_PASSWORD` | Mot de passe | - |
307
+ | `DB_DATABASE` / `DB_NAME` | Nom de la base | - |
308
+ | `DB_FILE` / `SQLITE_DB` | Fichier SQLite | `:memory:` |
309
+
310
+ ### Importation
311
+
312
+ ```javascript
313
+ // CommonJS - Import simple (connexion automatique via .env)
314
+ const { Model } = require('outlet-orm');
315
+
316
+ // ES Modules
317
+ import { Model } from 'outlet-orm';
318
+
319
+ // Si besoin de contrôle manuel sur la connexion
320
+ const { DatabaseConnection, Model } = require('outlet-orm');
321
+ ```
322
+
323
+ ### Définir un modèle
324
+
325
+ ```javascript
326
+ const { Model } = require('outlet-orm');
327
+
328
+ // Définition des modèles liés (voir Relations)
329
+ class Post extends Model { static table = 'posts'; }
330
+ class Profile extends Model { static table = 'profiles'; }
331
+
332
+ class User extends Model {
333
+ static table = 'users';
334
+ static primaryKey = 'id'; // Par défaut: 'id'
335
+ static timestamps = true; // Par défaut: true
336
+ static fillable = ['name', 'email', 'password'];
337
+ static hidden = ['password'];
338
+ static casts = {
339
+ id: 'int',
340
+ email_verified: 'boolean',
341
+ metadata: 'json',
342
+ birthday: 'date'
343
+ };
344
+
345
+ // Relations
346
+ posts() {
347
+ return this.hasMany(Post, 'user_id');
348
+ }
349
+
350
+ profile() {
351
+ return this.hasOne(Profile, 'user_id');
352
+ }
353
+ }
354
+ ```
355
+
356
+ ### Opérations CRUD
357
+
358
+ #### Créer
359
+
360
+ ```javascript
361
+ // Méthode 1: create()
362
+ const user = await User.create({
363
+ name: 'John Doe',
364
+ email: 'john@example.com',
365
+ password: 'secret123'
366
+ });
367
+
368
+ // Méthode 2: new + save()
369
+ const user = new User({
370
+ name: 'Jane Doe',
371
+ email: 'jane@example.com'
372
+ });
373
+ user.setAttribute('password', 'secret456');
374
+ await user.save();
375
+
376
+ // Insert brut (sans créer d'instance)
377
+ await User.insert({ name: 'Bob', email: 'bob@example.com' });
378
+ ```
379
+
380
+ #### Lire
381
+
382
+ ```javascript
383
+ // Tous les enregistrements
384
+ const users = await User.all();
385
+
386
+ // Par ID
387
+ const user = await User.find(1);
388
+ const user = await User.findOrFail(1); // Lance une erreur si non trouvé
389
+
390
+ // Premier résultat
391
+ const firstUser = await User.first();
392
+
393
+ // Avec conditions
394
+ const activeUsers = await User
395
+ .where('status', 'active')
396
+ .where('age', '>', 18)
397
+ .get();
398
+
399
+ // Avec relations (Eager Loading)
400
+ const usersWithPosts = await User
401
+ .with('posts', 'profile')
402
+ .get();
403
+
404
+ // Ordonner et limiter
405
+ const recentUsers = await User
406
+ .orderBy('created_at', 'desc')
407
+ .limit(10)
408
+ .get();
409
+ ```
410
+
411
+ #### Mettre à jour
412
+
413
+ ```javascript
414
+ // Instance
415
+ const user = await User.find(1);
416
+ user.setAttribute('name', 'Updated Name');
417
+ await user.save();
418
+
419
+ // Bulk update
420
+ await User
421
+ .where('status', 'pending')
422
+ .update({ status: 'active' });
423
+
424
+ // Update + Fetch (comme Prisma)
425
+ const updated = await User
426
+ .where('id', 1)
427
+ .updateAndFetch({ name: 'Neo' }, ['profile', 'posts']);
428
+
429
+ // Helpers par ID
430
+ const user = await User.updateAndFetchById(1, { name: 'Trinity' }, ['profile']);
431
+ await User.updateById(2, { status: 'active' });
432
+ ```
433
+
434
+ #### Supprimer
435
+
436
+ ```javascript
437
+ // Instance
438
+ const user = await User.find(1);
439
+ await user.destroy();
440
+
441
+ // Bulk delete
442
+ await User
443
+ .where('status', 'banned')
444
+ .delete();
445
+ ```
446
+
447
+ ### Query Builder
448
+
449
+ ```javascript
450
+ // Where clauses
451
+ const users = await User
452
+ .where('age', '>', 18)
453
+ .where('status', 'active')
454
+ .orWhere('role', 'admin')
455
+ .get();
456
+
457
+ // Where In / Not In
458
+ const users = await User.whereIn('id', [1, 2, 3, 4, 5]).get();
459
+ const users = await User.whereNotIn('status', ['banned', 'deleted']).get();
460
+
461
+ // Where Null / Not Null
462
+ const users = await User.whereNull('deleted_at').get();
463
+ const verified = await User.whereNotNull('email_verified_at').get();
464
+
465
+ // Where Between / Like
466
+ const adults = await User.whereBetween('age', [18, 65]).get();
467
+ const johns = await User.whereLike('name', '%john%').get();
468
+
469
+ // Pagination
470
+ const result = await User.paginate(1, 15);
471
+ // { data: [...], total: 100, per_page: 15, current_page: 1, last_page: 7, from: 1, to: 15 }
472
+
473
+ // Count / Exists
474
+ const count = await User.where('status', 'active').count();
475
+ const hasUsers = await User.where('role', 'admin').exists();
476
+
477
+ // Joins
478
+ const result = await User
479
+ .join('profiles', 'users.id', 'profiles.user_id')
480
+ .leftJoin('countries', 'profiles.country_id', 'countries.id')
481
+ .select('users.*', 'profiles.bio', 'countries.name as country')
482
+ .get();
483
+
484
+ // Agrégations
485
+ const stats = await User
486
+ .distinct()
487
+ .groupBy('status')
488
+ .having('COUNT(*)', '>', 5)
489
+ .get();
490
+
491
+ // Incrément / Décrément atomique
492
+ await User.where('id', 1).increment('login_count');
493
+ await User.where('id', 1).decrement('credits', 10);
494
+ ```
495
+
496
+ ### Filtres relationnels
497
+
498
+ ```javascript
499
+ // whereHas: Utilisateurs ayant au moins un post publié
500
+ const authors = await User
501
+ .whereHas('posts', (q) => {
502
+ q.where('status', 'published');
503
+ })
504
+ .get();
505
+
506
+ // has: Au moins N enfants
507
+ const prolific = await User.has('posts', '>=', 10).get();
508
+
509
+ // whereDoesntHave: Aucun enfant
510
+ const noPostUsers = await User.whereDoesntHave('posts').get();
511
+
512
+ // withCount: Ajouter une colonne {relation}_count
513
+ const withCounts = await User.withCount('posts').get();
514
+ // Chaque user aura: user.getAttribute('posts_count')
515
+ ```
516
+
517
+ ## 🔗 Relations
518
+
519
+ ### One to One (hasOne)
520
+
521
+ ```javascript
522
+ const { Model } = require('outlet-orm');
523
+
524
+ class Profile extends Model { static table = 'profiles'; }
525
+
526
+ class User extends Model {
527
+ static table = 'users';
528
+
529
+ profile() {
530
+ return this.hasOne(Profile, 'user_id');
531
+ }
532
+ }
533
+
534
+ const user = await User.find(1);
535
+ const profile = await user.profile().get();
536
+ ```
537
+
538
+ ### One to Many (hasMany)
539
+
540
+ ```javascript
541
+ const { Model } = require('outlet-orm');
542
+
543
+ class Post extends Model { static table = 'posts'; }
544
+
545
+ class User extends Model {
546
+ static table = 'users';
547
+
548
+ posts() {
549
+ return this.hasMany(Post, 'user_id');
550
+ }
551
+ }
552
+
553
+ const user = await User.find(1);
554
+ const posts = await user.posts().get();
555
+ ```
556
+
557
+ ### Belongs To (belongsTo)
558
+
559
+ ```javascript
560
+ const { Model } = require('outlet-orm');
561
+
562
+ class User extends Model { static table = 'users'; }
563
+
564
+ class Post extends Model {
565
+ static table = 'posts';
566
+
567
+ author() {
568
+ return this.belongsTo(User, 'user_id');
569
+ }
570
+ }
571
+
572
+ const post = await Post.find(1);
573
+ const author = await post.author().get();
574
+ ```
575
+
576
+ ### Many to Many (belongsToMany)
577
+
578
+ ```javascript
579
+ const { Model } = require('outlet-orm');
580
+
581
+ class Role extends Model { static table = 'roles'; }
582
+
583
+ class User extends Model {
584
+ static table = 'users';
585
+
586
+ roles() {
587
+ return this.belongsToMany(
588
+ Role,
589
+ 'user_roles', // Table pivot
590
+ 'user_id', // FK vers User
591
+ 'role_id' // FK vers Role
592
+ );
593
+ }
594
+ }
595
+
596
+ const user = await User.find(1);
597
+ const roles = await user.roles().get();
598
+
599
+ // Méthodes pivot
600
+ await user.roles().attach([1, 2]); // Attacher des rôles
601
+ await user.roles().detach(2); // Détacher un rôle
602
+ await user.roles().sync([1, 3]); // Synchroniser (remplace tout)
603
+ ```
604
+
605
+ ### Has Many Through (hasManyThrough)
606
+
607
+ Accéder à une relation distante via un modèle intermédiaire.
608
+
609
+ ```javascript
610
+ const { Model } = require('outlet-orm');
611
+
612
+ class User extends Model {
613
+ // User -> Post -> Comment
614
+ comments() {
615
+ return this.hasManyThrough(Comment, Post, 'user_id', 'post_id');
616
+ }
617
+ }
618
+
619
+ const user = await User.find(1);
620
+ const allComments = await user.comments().get();
621
+ ```
622
+
623
+ ### Has One Through (hasOneThrough)
624
+
625
+ ```javascript
626
+ const { Model } = require('outlet-orm');
627
+
628
+ class User extends Model {
629
+ // User -> Profile -> Country
630
+ country() {
631
+ return this.hasOneThrough(Country, Profile, 'user_id', 'country_id');
632
+ }
633
+ }
634
+
635
+ const user = await User.find(1);
636
+ const country = await user.country().get();
637
+ ```
638
+
639
+ ### Relations Polymorphiques
640
+
641
+ Les relations polymorphiques permettent à un modèle d'appartenir à plusieurs autres modèles.
642
+
643
+ ```javascript
644
+ const { Model } = require('outlet-orm');
645
+
646
+ // Configuration du morph map
647
+ Model.setMorphMap({
648
+ 'posts': Post,
649
+ 'videos': Video
650
+ });
651
+
652
+ // Modèles
653
+ class Post extends Model {
654
+ comments() {
655
+ return this.morphMany(Comment, 'commentable');
656
+ }
657
+ }
658
+
659
+ class Video extends Model {
660
+ comments() {
661
+ return this.morphMany(Comment, 'commentable');
662
+ }
663
+ }
664
+
665
+ class Comment extends Model {
666
+ commentable() {
667
+ return this.morphTo('commentable');
668
+ }
669
+ }
670
+
671
+ // Usage
672
+ const post = await Post.find(1);
673
+ const comments = await post.comments().get();
674
+
675
+ const comment = await Comment.find(1);
676
+ const parent = await comment.commentable().get(); // Post ou Video
677
+ ```
678
+
679
+ **Relations polymorphiques disponibles:**
680
+ - `morphOne(Related, 'morphName')` - One-to-One polymorphique
681
+ - `morphMany(Related, 'morphName')` - One-to-Many polymorphique
682
+ - `morphTo('morphName')` - Inverse polymorphique
683
+
684
+ ### Eager Loading
685
+
686
+ ```javascript
687
+ // Charger plusieurs relations
688
+ const users = await User.with('posts', 'profile', 'roles').get();
689
+
690
+ // Charger avec contraintes
691
+ const users = await User.with({
692
+ posts: (q) => q.where('status', 'published').orderBy('created_at', 'desc')
693
+ }).get();
694
+
695
+ // Charger des relations imbriquées (dot notation)
696
+ const users = await User.with('posts.comments.author').get();
697
+
698
+ // Charger sur une instance existante
699
+ const user = await User.find(1);
700
+ await user.load('posts', 'profile');
701
+ await user.load(['roles', 'posts.comments']);
702
+
703
+ // Accéder aux relations chargées
704
+ users.forEach(user => {
705
+ console.log(user.relations.posts);
706
+ console.log(user.relations.profile);
707
+ });
708
+ ```
709
+
710
+ ## 🎭 Attributs
711
+
712
+ ### Casts
713
+
714
+ Les casts convertissent automatiquement les attributs:
715
+
716
+ ```javascript
717
+ const { Model } = require('outlet-orm');
718
+
719
+ class User extends Model {
720
+ static casts = {
721
+ id: 'int', // ou 'integer'
722
+ age: 'integer',
723
+ balance: 'float', // ou 'double'
724
+ email_verified: 'boolean', // ou 'bool'
725
+ metadata: 'json', // Parse JSON
726
+ settings: 'array', // Parse JSON en array
727
+ birthday: 'date' // Convertit en Date
728
+ };
729
+ }
730
+ ```
731
+
732
+ ### Attributs cachés
733
+
734
+ ```javascript
735
+ const { Model } = require('outlet-orm');
736
+
737
+ class User extends Model {
738
+ static hidden = ['password', 'secret_token'];
739
+ }
740
+
741
+ const user = await User.find(1);
742
+ console.log(user.toJSON()); // password et secret_token exclus
743
+ ```
744
+
745
+ #### Afficher les attributs cachés
746
+
747
+ ```javascript
748
+ // Inclure les attributs cachés
749
+ const user = await User.withHidden().where('email', 'john@example.com').first();
750
+ console.log(user.toJSON()); // password inclus
751
+
752
+ // Contrôler avec un booléen
753
+ const user = await User.withoutHidden(true).first(); // true = afficher
754
+ const user = await User.withoutHidden(false).first(); // false = masquer (défaut)
755
+
756
+ // Cas d'usage: authentification
757
+ const user = await User.withHidden().where('email', email).first();
758
+ if (user && await bcrypt.compare(password, user.getAttribute('password'))) {
759
+ // Authentification réussie
760
+ }
761
+ ```
762
+
763
+ ### Timestamps
764
+
765
+ ```javascript
766
+ const { Model } = require('outlet-orm');
767
+
768
+ // Activés par défaut (created_at, updated_at)
769
+ class User extends Model {
770
+ static timestamps = true;
771
+ }
772
+
773
+ // Désactiver
774
+ class Log extends Model {
775
+ static timestamps = false;
776
+ }
777
+ ```
778
+
779
+ ## 🔄 Transactions
780
+
781
+ Outlet ORM supporte les transactions pour garantir l'intégrité des données:
782
+
783
+ ```javascript
784
+ const { DatabaseConnection, Model } = require('outlet-orm');
785
+
786
+ // Méthode 1: Callback automatique (recommandé)
787
+ const db = Model.connection;
788
+ const result = await db.transaction(async (connection) => {
789
+ const user = await User.create({ name: 'John', email: 'john@example.com' });
790
+ await Account.create({ user_id: user.getAttribute('id'), balance: 0 });
791
+ return user;
792
+ });
793
+ // Commit automatique, rollback si erreur
794
+
795
+ // Méthode 2: Contrôle manuel
796
+ await db.beginTransaction();
797
+ try {
798
+ await User.create({ name: 'Jane' });
799
+ await db.commit();
800
+ } catch (error) {
801
+ await db.rollback();
802
+ throw error;
803
+ }
804
+ ```
805
+
806
+ ## 🗑️ Soft Deletes
807
+
808
+ Suppression logique avec colonne `deleted_at`:
809
+
810
+ ```javascript
811
+ const { Model } = require('outlet-orm');
812
+
813
+ class Post extends Model {
814
+ static table = 'posts';
815
+ static softDeletes = true;
816
+ // static DELETED_AT = 'deleted_at'; // Personnalisable
817
+ }
818
+
819
+ // Les requêtes excluent automatiquement les supprimés
820
+ const posts = await Post.all(); // Seulement les non-supprimés
821
+
822
+ // Inclure les supprimés
823
+ const allPosts = await Post.withTrashed().get();
824
+
825
+ // Seulement les supprimés
826
+ const trashedPosts = await Post.onlyTrashed().get();
827
+
828
+ // Supprimer (soft delete)
829
+ const post = await Post.find(1);
830
+ await post.destroy(); // Met deleted_at à la date actuelle
831
+
832
+ // Vérifier si supprimé
833
+ if (post.trashed()) {
834
+ console.log('Ce post est supprimé');
835
+ }
836
+
837
+ // Restaurer
838
+ await post.restore();
839
+
840
+ // Supprimer définitivement
841
+ await post.forceDelete();
842
+ ```
843
+
844
+ ## 🔬 Scopes
845
+
846
+ ### Scopes Globaux
847
+
848
+ Appliqués automatiquement à toutes les requêtes:
849
+
850
+ ```javascript
851
+ const { Model } = require('outlet-orm');
852
+
853
+ class Post extends Model {
854
+ static table = 'posts';
855
+ }
856
+
857
+ // Ajouter un scope global
858
+ Post.addGlobalScope('published', (query) => {
859
+ query.where('status', 'published');
860
+ });
861
+
862
+ // Toutes les requêtes filtrent automatiquement
863
+ const posts = await Post.all(); // Seulement les publiés
864
+
865
+ // Désactiver temporairement un scope
866
+ const allPosts = await Post.withoutGlobalScope('published').get();
867
+
868
+ // Désactiver tous les scopes
869
+ const rawPosts = await Post.withoutGlobalScopes().get();
870
+ ```
871
+
872
+ ## 📣 Events / Hooks
873
+
874
+ Interceptez les opérations sur vos modèles:
875
+
876
+ ```javascript
877
+ const { Model } = require('outlet-orm');
878
+
879
+ class User extends Model {
880
+ static table = 'users';
881
+ }
882
+
883
+ // Avant création
884
+ User.creating((user) => {
885
+ user.setAttribute('uuid', generateUUID());
886
+ // Retourner false pour annuler
887
+ });
888
+
889
+ // Après création
890
+ User.created((user) => {
891
+ console.log(`Utilisateur ${user.getAttribute('id')} créé`);
892
+ });
893
+
894
+ // Avant mise à jour
895
+ User.updating((user) => {
896
+ user.setAttribute('updated_at', new Date());
897
+ });
898
+
899
+ // Après mise à jour
900
+ User.updated((user) => {
901
+ // Notifier les systèmes externes
902
+ });
903
+
904
+ // Événements saving/saved (création ET mise à jour)
905
+ User.saving((user) => {
906
+ // Nettoyage des données
907
+ });
908
+
909
+ User.saved((user) => {
910
+ // Cache invalidation
911
+ });
912
+
913
+ // Avant/après suppression
914
+ User.deleting((user) => {
915
+ // Vérifications avant suppression
916
+ });
917
+
918
+ User.deleted((user) => {
919
+ // Nettoyage des relations
920
+ });
921
+
922
+ // Pour les soft deletes
923
+ User.restoring((user) => {});
924
+ User.restored((user) => {});
925
+ ```
926
+
927
+ ## Validation
928
+
929
+ Validation basique intégrée:
930
+
931
+ ```javascript
932
+ const { Model } = require('outlet-orm');
933
+
934
+ class User extends Model {
935
+ static table = 'users';
936
+ static rules = {
937
+ name: 'required|string|min:2|max:100',
938
+ email: 'required|email',
939
+ age: 'numeric|min:0|max:150',
940
+ role: 'in:admin,user,guest',
941
+ password: 'required|min:8'
942
+ };
943
+ }
944
+
945
+ const user = new User({
946
+ name: 'J',
947
+ email: 'invalid-email',
948
+ age: 200
949
+ });
950
+
951
+ // Valider
952
+ const { valid, errors } = user.validate();
953
+ console.log(valid); // false
954
+ console.log(errors);
955
+ // {
956
+ // name: ['name must be at least 2 characters'],
957
+ // email: ['email must be a valid email'],
958
+ // age: ['age must not exceed 150']
959
+ // }
960
+
961
+ // Valider ou lancer une erreur
962
+ try {
963
+ user.validateOrFail();
964
+ } catch (error) {
965
+ console.log(error.errors);
966
+ }
967
+ ```
968
+
969
+ ### Règles disponibles
970
+
971
+ | Règle | Description |
972
+ |-------|-------------|
973
+ | `required` | Champ obligatoire |
974
+ | `string` | Doit être une chaîne |
975
+ | `number` / `numeric` | Doit être un nombre |
976
+ | `email` | Format email valide |
977
+ | `boolean` | Doit être un booléen |
978
+ | `date` | Date valide |
979
+ | `min:N` | Minimum N (longueur ou valeur) |
980
+ | `max:N` | Maximum N (longueur ou valeur) |
981
+ | `in:a,b,c` | Valeur parmi la liste |
982
+ | `regex:pattern` | Match le pattern regex |
983
+
984
+ ## 📊 Query Logging
985
+
986
+ Mode debug pour analyser vos requêtes:
987
+
988
+ ```javascript
989
+ const { Model } = require('outlet-orm');
990
+
991
+ // Activer le logging
992
+ const db = Model.getConnection();
993
+ db.enableQueryLog();
994
+
995
+ // Exécuter des requêtes
996
+ await User.where('status', 'active').get();
997
+ await Post.with('author').get();
998
+
999
+ // Récupérer le log
1000
+ const queries = db.getQueryLog();
1001
+ console.log(queries);
1002
+ // [
1003
+ // { sql: 'SELECT * FROM users WHERE status = ?', params: ['active'], duration: 15, timestamp: Date },
1004
+ // { sql: 'SELECT * FROM posts', params: [], duration: 8, timestamp: Date }
1005
+ // ]
1006
+
1007
+ // Vider le log
1008
+ db.flushQueryLog();
1009
+
1010
+ // Désactiver le logging
1011
+ db.disableQueryLog();
1012
+
1013
+ // Vérifier si actif
1014
+ if (db.isLogging()) {
1015
+ console.log('Logging actif');
1016
+ }
1017
+ ```
1018
+
1019
+ ## 📝 API Reference
1020
+
1021
+ ### DatabaseConnection
1022
+
1023
+ | Méthode | Description |
1024
+ |---------|-------------|
1025
+ | `new DatabaseConnection(config?)` | Crée une connexion (lit `.env` si config omis) |
1026
+ | `connect()` | Établit la connexion (appelé automatiquement) |
1027
+ | `beginTransaction()` | Démarre une transaction |
1028
+ | `commit()` | Valide la transaction |
1029
+ | `rollback()` | Annule la transaction |
1030
+ | `transaction(callback)` | Exécute dans une transaction (auto commit/rollback) |
1031
+ | `select(table, query)` | Exécute un SELECT |
1032
+ | `insert(table, data)` | Insère un enregistrement |
1033
+ | `insertMany(table, data[])` | Insère plusieurs enregistrements |
1034
+ | `update(table, data, query)` | Met à jour des enregistrements |
1035
+ | `delete(table, query)` | Supprime des enregistrements |
1036
+ | `count(table, query)` | Compte les enregistrements |
1037
+ | `executeRawQuery(sql, params?)` | Requête brute (résultats normalisés) |
1038
+ | `execute(sql, params?)` | Requête brute (résultats natifs driver) |
1039
+ | `increment(table, column, query, amount?)` | Incrément atomique |
1040
+ | `decrement(table, column, query, amount?)` | Décrément atomique |
1041
+ | `close()` / `disconnect()` | Ferme la connexion |
1042
+ | **Query Logging (static)** | |
1043
+ | `enableQueryLog()` | Active le logging des requêtes |
1044
+ | `disableQueryLog()` | Désactive le logging |
1045
+ | `getQueryLog()` | Retourne le log des requêtes |
1046
+ | `flushQueryLog()` | Vide le log |
1047
+ | `isLogging()` | Vérifie si le logging est actif |
1048
+
1049
+ ### Model (méthodes statiques)
1050
+
1051
+ | Méthode | Description |
1052
+ |---------|-------------|
1053
+ | `setConnection(db)` | Définit la connexion par défaut |
1054
+ | `getConnection()` | Récupère la connexion (v3.0.0+) |
1055
+ | `setMorphMap(map)` | Définit le mapping polymorphique |
1056
+ | `query()` | Retourne un QueryBuilder |
1057
+ | `all()` | Tous les enregistrements |
1058
+ | `find(id)` | Trouve par ID |
1059
+ | `findOrFail(id)` | Trouve ou lance une erreur |
1060
+ | `first()` | Premier enregistrement |
1061
+ | `where(col, op?, val)` | Clause WHERE |
1062
+ | `whereIn(col, vals)` | Clause WHERE IN |
1063
+ | `whereNull(col)` | Clause WHERE NULL |
1064
+ | `whereNotNull(col)` | Clause WHERE NOT NULL |
1065
+ | `create(attrs)` | Crée et sauvegarde |
1066
+ | `insert(data)` | Insert brut |
1067
+ | `update(attrs)` | Update bulk |
1068
+ | `updateById(id, attrs)` | Update par ID |
1069
+ | `updateAndFetchById(id, attrs, rels?)` | Update + fetch avec relations |
1070
+ | `delete()` | Delete bulk |
1071
+ | `with(...rels)` | Eager loading |
1072
+ | `withHidden()` | Inclut les attributs cachés |
1073
+ | `withoutHidden(show?)` | Contrôle visibilité |
1074
+ | `orderBy(col, dir?)` | Tri |
1075
+ | `limit(n)` / `offset(n)` | Limite/Offset |
1076
+ | `paginate(page, perPage)` | Pagination |
1077
+ | `count()` | Compte |
1078
+ | **Soft Deletes** | |
1079
+ | `withTrashed()` | Inclut les supprimés |
1080
+ | `onlyTrashed()` | Seulement les supprimés |
1081
+ | **Scopes** | |
1082
+ | `addGlobalScope(name, cb)` | Ajoute un scope global |
1083
+ | `removeGlobalScope(name)` | Supprime un scope |
1084
+ | `withoutGlobalScope(name)` | Requête sans un scope |
1085
+ | `withoutGlobalScopes()` | Requête sans tous les scopes |
1086
+ | **Events** | |
1087
+ | `on(event, callback)` | Enregistre un listener |
1088
+ | `creating(cb)` / `created(cb)` | Events création |
1089
+ | `updating(cb)` / `updated(cb)` | Events mise à jour |
1090
+ | `saving(cb)` / `saved(cb)` | Events sauvegarde |
1091
+ | `deleting(cb)` / `deleted(cb)` | Events suppression |
1092
+ | `restoring(cb)` / `restored(cb)` | Events restauration |
1093
+
1094
+ ### Model (méthodes d'instance)
1095
+
1096
+ | Méthode | Description |
1097
+ |---------|-------------|
1098
+ | `fill(attrs)` | Remplit les attributs |
1099
+ | `setAttribute(key, val)` | Définit un attribut |
1100
+ | `getAttribute(key)` | Récupère un attribut |
1101
+ | `save()` | Sauvegarde (insert ou update) |
1102
+ | `destroy()` | Supprime l'instance (soft si activé) |
1103
+ | `load(...rels)` | Charge des relations |
1104
+ | `getDirty()` | Attributs modifiés |
1105
+ | `isDirty()` | A été modifié? |
1106
+ | `toJSON()` | Convertit en objet |
1107
+ | **Soft Deletes** | |
1108
+ | `trashed()` | Est supprimé? |
1109
+ | `restore()` | Restaure le modèle |
1110
+ | `forceDelete()` | Suppression définitive |
1111
+ | **Validation** | |
1112
+ | `validate()` | Valide selon les règles |
1113
+ | `validateOrFail()` | Valide ou lance erreur |
1114
+
1115
+ ### QueryBuilder
1116
+
1117
+ | Méthode | Description |
1118
+ |---------|-------------|
1119
+ | `select(...cols)` / `columns([...])` | Sélection de colonnes |
1120
+ | `distinct()` | SELECT DISTINCT |
1121
+ | `where(col, op?, val)` | Clause WHERE |
1122
+ | `whereIn(col, vals)` | WHERE IN |
1123
+ | `whereNotIn(col, vals)` | WHERE NOT IN |
1124
+ | `whereNull(col)` | WHERE NULL |
1125
+ | `whereNotNull(col)` | WHERE NOT NULL |
1126
+ | `orWhere(col, op?, val)` | OR WHERE |
1127
+ | `whereBetween(col, [min, max])` | WHERE BETWEEN |
1128
+ | `whereLike(col, pattern)` | WHERE LIKE |
1129
+ | `whereHas(rel, cb?)` | Filtre par relation |
1130
+ | `has(rel, op?, count)` | Existence relationnelle |
1131
+ | `whereDoesntHave(rel)` | Absence de relation |
1132
+ | `orderBy(col, dir?)` / `ordrer(...)` | Tri |
1133
+ | `limit(n)` / `take(n)` | Limite |
1134
+ | `offset(n)` / `skip(n)` | Offset |
1135
+ | `groupBy(...cols)` | GROUP BY |
1136
+ | `having(col, op, val)` | HAVING |
1137
+ | `join(table, first, op?, second)` | INNER JOIN |
1138
+ | `leftJoin(table, first, op?, second)` | LEFT JOIN |
1139
+ | `with(...rels)` | Eager loading |
1140
+ | `withCount(rels)` | Ajoute {rel}_count |
1141
+ | `withTrashed()` | Inclut les supprimés |
1142
+ | `onlyTrashed()` | Seulement les supprimés |
1143
+ | `withoutGlobalScope(name)` | Sans un scope global |
1144
+ | `withoutGlobalScopes()` | Sans tous les scopes |
1145
+ | `get()` | Exécute et retourne tous |
1146
+ | `first()` | Premier résultat |
1147
+ | `firstOrFail()` | Premier ou erreur |
1148
+ | `paginate(page, perPage)` | Pagination |
1149
+ | `count()` | Compte |
1150
+ | `exists()` | Vérifie l'existence |
1151
+ | `insert(data)` | Insert |
1152
+ | `update(attrs)` | Update |
1153
+ | `updateAndFetch(attrs, rels?)` | Update + fetch |
1154
+ | `delete()` | Delete |
1155
+ | `increment(col, amount?)` | Incrément atomique |
1156
+ | `decrement(col, amount?)` | Décrément atomique |
1157
+ | `clone()` | Clone le query builder |
1158
+
1159
+ ## 🛠️ Outils CLI
1160
+
1161
+ ### outlet-init
1162
+
1163
+ Initialise un nouveau projet avec configuration de base de données.
1164
+
1165
+ ```bash
1166
+ outlet-init
1167
+ ```
1168
+
1169
+ Génère:
1170
+ - Fichier de configuration `database/config.js`
1171
+ - Fichier `.env` avec les paramètres
1172
+ - Modèle exemple
1173
+ - Fichier d'utilisation
1174
+
1175
+ ### outlet-migrate
1176
+
1177
+ Système complet de migrations.
1178
+
1179
+ ```bash
1180
+ # Créer une migration
1181
+ outlet-migrate make create_users_table
1182
+
1183
+ # Exécuter les migrations
1184
+ outlet-migrate migrate
1185
+
1186
+ # Voir le statut
1187
+ outlet-migrate status
1188
+
1189
+ # Annuler la dernière migration
1190
+ outlet-migrate rollback --steps 1
1191
+
1192
+ # Reset toutes les migrations
1193
+ outlet-migrate reset --yes
1194
+
1195
+ # Refresh (reset + migrate)
1196
+ outlet-migrate refresh --yes
1197
+
1198
+ # Fresh (drop all + migrate)
1199
+ outlet-migrate fresh --yes
1200
+ ```
1201
+
1202
+ **Fonctionnalités des Migrations:**
1203
+
1204
+ - ✅ Création et gestion des migrations (create, alter, drop tables)
1205
+ - ✅ Types de colonnes: id, string, text, integer, boolean, date, datetime, timestamp, decimal, float, json, enum, uuid, foreignId
1206
+ - Modificateurs: nullable, default, unique, index, unsigned, autoIncrement, comment, after, first
1207
+ - ✅ Clés étrangères: foreign(), constrained(), onDelete(), onUpdate(), CASCADE
1208
+ - Index: index(), unique(), fullText()
1209
+ - ✅ Manipulation: renameColumn(), dropColumn(), dropTimestamps()
1210
+ - ✅ Migrations réversibles: Méthodes up() et down()
1211
+ - ✅ Batch tracking: Rollback précis par batch
1212
+ - SQL personnalisé: execute() pour commandes avancées
1213
+
1214
+ ### outlet-convert
1215
+
1216
+ Convertit des schémas SQL en modèles ORM.
1217
+
1218
+ ```bash
1219
+ outlet-convert
1220
+ ```
1221
+
1222
+ **Options:**
1223
+ 1. Depuis un fichier SQL local
1224
+ 2. Depuis une base de données connectée
1225
+
1226
+ **Fonctionnalités:**
1227
+ - ✅ Détection automatique des types et casts
1228
+ - ✅ Génération automatique de TOUTES les relations (belongsTo, hasMany, hasOne, belongsToMany)
1229
+ - ✅ Relations récursives (auto-relations)
1230
+ - ✅ Détection des champs sensibles (password, token, etc.)
1231
+ - ✅ Support des timestamps automatiques
1232
+ - ✅ Conversion des noms en PascalCase
1233
+
1234
+ ## 📚 Documentation
1235
+
1236
+ - [Guide des Migrations](docs/MIGRATIONS.md)
1237
+ - [Conversion SQL](docs/SQL_CONVERSION.md)
1238
+ - [Détection des Relations](docs/RELATIONS_DETECTION.md)
1239
+ - [Guide de démarrage rapide](docs/QUICKSTART.md)
1240
+ - [Architecture](docs/ARCHITECTURE.md)
1241
+ - [**TypeScript (complet)**](docs/TYPESCRIPT.md)
1242
+
1243
+ ## 📘 TypeScript Support
1244
+
1245
+ Outlet ORM v4.0.0 inclut des définitions TypeScript complètes avec support des **generics pour les attributs typés**.
1246
+
1247
+ ### Modèles typés
1248
+
1249
+ ```typescript
1250
+ import { Model, HasManyRelation } from 'outlet-orm';
1251
+
1252
+ interface UserAttributes {
1253
+ id: number;
1254
+ name: string;
1255
+ email: string;
1256
+ role: 'admin' | 'user';
1257
+ created_at: Date;
1258
+ }
1259
+
1260
+ class User extends Model<UserAttributes> {
1261
+ static table = 'users';
1262
+ static fillable = ['name', 'email', 'role'];
1263
+
1264
+ posts(): HasManyRelation<Post> {
1265
+ return this.hasMany(Post, 'user_id');
1266
+ }
1267
+ }
1268
+
1269
+ // Type-safe getAttribute/setAttribute
1270
+ const user = await User.find(1);
1271
+ const name: string = user.getAttribute('name'); // ✅ Type inféré
1272
+ const role: 'admin' | 'user' = user.getAttribute('role');
1273
+ ```
1274
+
1275
+ ### Migrations typées
1276
+
1277
+ ```typescript
1278
+ import { MigrationInterface, Schema, TableBuilder } from 'outlet-orm';
1279
+
1280
+ export const migration: MigrationInterface = {
1281
+ name: 'create_users_table',
1282
+
1283
+ async up(): Promise<void> {
1284
+ await Schema.create('users', (table: TableBuilder) => {
1285
+ table.id();
1286
+ table.string('name');
1287
+ table.string('email').unique();
1288
+ table.timestamps();
1289
+ });
1290
+ },
1291
+
1292
+ async down(): Promise<void> {
1293
+ await Schema.dropIfExists('users');
1294
+ }
1295
+ };
1296
+ ```
1297
+
1298
+ 📖 [Guide TypeScript complet](docs/TYPESCRIPT.md)
1299
+
1300
+ ## 🤝 Contribution
1301
+
1302
+ Les contributions sont les bienvenues! N'hésitez pas à ouvrir une issue ou un pull request.
1303
+
1304
+ Voir [CONTRIBUTING.md](CONTRIBUTING.md) pour les guidelines.
1305
+
1306
+ ## 📄 Licence
1307
+
1308
+ MIT - Voir [LICENSE](LICENSE) pour plus de détails.
1309
+
1310
+ ---
1311
+
1312
+ Créé par [omgbwa-yasse](https://github.com/omgbwa-yasse)