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