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