outlet-orm 5.0.0 → 5.5.1

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