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