outlet-orm 2.5.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/LICENSE +21 -0
- package/README.md +705 -0
- package/bin/convert.js +679 -0
- package/bin/init.js +190 -0
- package/bin/migrate.js +442 -0
- package/lib/Database/DatabaseConnection.js +4 -0
- package/lib/Migrations/Migration.js +48 -0
- package/lib/Migrations/MigrationManager.js +326 -0
- package/lib/Schema/Schema.js +790 -0
- package/package.json +75 -0
- package/src/DatabaseConnection.js +697 -0
- package/src/Model.js +659 -0
- package/src/QueryBuilder.js +710 -0
- package/src/Relations/BelongsToManyRelation.js +466 -0
- package/src/Relations/BelongsToRelation.js +127 -0
- package/src/Relations/HasManyRelation.js +125 -0
- package/src/Relations/HasManyThroughRelation.js +112 -0
- package/src/Relations/HasOneRelation.js +114 -0
- package/src/Relations/HasOneThroughRelation.js +105 -0
- package/src/Relations/MorphManyRelation.js +69 -0
- package/src/Relations/MorphOneRelation.js +68 -0
- package/src/Relations/MorphToRelation.js +110 -0
- package/src/Relations/Relation.js +31 -0
- package/src/index.js +23 -0
- package/types/index.d.ts +272 -0
package/README.md
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
# Outlet ORM
|
|
2
|
+
|
|
3
|
+
Un ORM JavaScript inspiré de Laravel Eloquent pour Node.js avec support pour MySQL, PostgreSQL et SQLite.
|
|
4
|
+
|
|
5
|
+
## ✅ Prérequis et compatibilité
|
|
6
|
+
|
|
7
|
+
- Node.js >= 18 (recommandé/exigé)
|
|
8
|
+
- Installez le driver de base de données correspondant à votre SGBD (voir ci-dessous)
|
|
9
|
+
|
|
10
|
+
## 🚀 Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install outlet-orm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Installer le driver de base de données
|
|
17
|
+
|
|
18
|
+
Outlet ORM utilise des peerDependencies optionnelles pour les drivers de base de données. Installez uniquement le driver dont vous avez besoin:
|
|
19
|
+
|
|
20
|
+
- MySQL/MariaDB: `npm install mysql2`
|
|
21
|
+
- PostgreSQL: `npm install pg`
|
|
22
|
+
- SQLite: `npm install sqlite3`
|
|
23
|
+
|
|
24
|
+
Si aucun driver n'est installé, un message d'erreur explicite vous indiquera lequel installer lors de la connexion.
|
|
25
|
+
|
|
26
|
+
## ✨ Fonctionnalités clés
|
|
27
|
+
|
|
28
|
+
- API inspirée d'Eloquent (Active Record) pour un usage fluide
|
|
29
|
+
- Query Builder expressif: where/joins/order/limit/offset/paginate
|
|
30
|
+
- Filtres relationnels façon Laravel: `whereHas()`
|
|
31
|
+
- Existence/absence et agrégations: `has()`, `whereDoesntHave()`, `withCount()`
|
|
32
|
+
- Eager Loading des relations via `.with(...)`
|
|
33
|
+
- Relations: hasOne, hasMany, belongsTo, belongsToMany (avec attach/detach/sync)
|
|
34
|
+
- Casts automatiques (int, float, boolean, json, date...)
|
|
35
|
+
- Attributs masqués (`hidden`) et timestamps automatiques
|
|
36
|
+
- Incrément/Décrément atomiques: `increment()` et `decrement()`
|
|
37
|
+
- Aliases ergonomiques: `columns([...])`, `ordrer()` (alias typo de `orderBy`)
|
|
38
|
+
- Requêtes brutes: `executeRawQuery()` et `execute()` (résultats natifs du driver)
|
|
39
|
+
- Migrations complètes (create/alter/drop, index, foreign keys, batch tracking)
|
|
40
|
+
- CLI pratiques: `outlet-init`, `outlet-migrate`, `outlet-convert`
|
|
41
|
+
- Configuration via `.env` (chargée automatiquement)
|
|
42
|
+
- Multi-base de données: MySQL, PostgreSQL et SQLite
|
|
43
|
+
- Types TypeScript fournis
|
|
44
|
+
|
|
45
|
+
## ⚡ Démarrage Rapide
|
|
46
|
+
|
|
47
|
+
### Initialisation du projet
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Créer la configuration initiale
|
|
51
|
+
outlet-init
|
|
52
|
+
|
|
53
|
+
# Créer une migration
|
|
54
|
+
outlet-migrate make create_users_table
|
|
55
|
+
|
|
56
|
+
# Exécuter les migrations
|
|
57
|
+
outlet-migrate
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 📖 Utilisation Rapide
|
|
61
|
+
|
|
62
|
+
### Configuration de la connexion
|
|
63
|
+
|
|
64
|
+
Outlet ORM peut charger automatiquement le provider (driver) et les paramètres d’accès à la base de données depuis un fichier `.env` dans votre application. Les variables supportées incluent :
|
|
65
|
+
|
|
66
|
+
- DB_DRIVER (mysql, postgres, sqlite)
|
|
67
|
+
- DB_HOST, DB_PORT
|
|
68
|
+
- DB_USER / DB_USERNAME, DB_PASSWORD
|
|
69
|
+
- DB_DATABASE / DB_NAME
|
|
70
|
+
- Pour SQLite: DB_FILE ou SQLITE_DB ou SQLITE_FILENAME
|
|
71
|
+
|
|
72
|
+
Un exemple est fourni dans `.env.example`.
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
const { DatabaseConnection, Model } = require('outlet-orm');
|
|
76
|
+
|
|
77
|
+
// Configuration MySQL
|
|
78
|
+
// 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
|
+
const db = new DatabaseConnection();
|
|
81
|
+
|
|
82
|
+
// Option 2 – via objet de configuration (prend le dessus sur .env)
|
|
83
|
+
// const db = new DatabaseConnection({
|
|
84
|
+
// driver: 'mysql',
|
|
85
|
+
// host: 'localhost',
|
|
86
|
+
// database: 'myapp',
|
|
87
|
+
// user: 'root',
|
|
88
|
+
// password: 'secret',
|
|
89
|
+
// port: 3306
|
|
90
|
+
// });
|
|
91
|
+
|
|
92
|
+
// Définir la connexion par défaut
|
|
93
|
+
Model.setConnection(db);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Variables d'environnement (.env) — Détails
|
|
97
|
+
|
|
98
|
+
- DB_DRIVER: `mysql` | `postgres` | `sqlite` (alias acceptés: `postgresql`, `sqlite3`)
|
|
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:`)
|
|
103
|
+
|
|
104
|
+
Les paramètres passés au constructeur de `DatabaseConnection` ont priorité sur `.env`.
|
|
105
|
+
|
|
106
|
+
### Définir un modèle
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
class User extends Model {
|
|
110
|
+
static table = 'users';
|
|
111
|
+
static fillable = ['name', 'email', 'password'];
|
|
112
|
+
static hidden = ['password'];
|
|
113
|
+
static casts = {
|
|
114
|
+
id: 'int',
|
|
115
|
+
email_verified: 'boolean',
|
|
116
|
+
metadata: 'json'
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Relations
|
|
120
|
+
posts() {
|
|
121
|
+
return this.hasMany(Post, 'user_id');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
profile() {
|
|
125
|
+
return this.hasOne(Profile, 'user_id');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Opérations CRUD
|
|
131
|
+
|
|
132
|
+
#### Créer
|
|
133
|
+
|
|
134
|
+
```javascript
|
|
135
|
+
// Méthode 1: create()
|
|
136
|
+
const user = await User.create({
|
|
137
|
+
name: 'John Doe',
|
|
138
|
+
email: 'john@example.com',
|
|
139
|
+
password: 'secret123'
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// Méthode 2: new + save()
|
|
143
|
+
const user = new User({
|
|
144
|
+
name: 'Jane Doe',
|
|
145
|
+
email: 'jane@example.com'
|
|
146
|
+
});
|
|
147
|
+
user.setAttribute('password', 'secret456');
|
|
148
|
+
await user.save();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Lire
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
// Tous les enregistrements
|
|
155
|
+
const users = await User.all();
|
|
156
|
+
|
|
157
|
+
// Par ID
|
|
158
|
+
const user = await User.find(1);
|
|
159
|
+
|
|
160
|
+
// Premier résultat
|
|
161
|
+
const firstUser = await User.first();
|
|
162
|
+
|
|
163
|
+
// Avec conditions
|
|
164
|
+
const activeUsers = await User
|
|
165
|
+
.where('status', 'active')
|
|
166
|
+
.where('age', '>', 18)
|
|
167
|
+
.get();
|
|
168
|
+
|
|
169
|
+
// Avec relations
|
|
170
|
+
const usersWithPosts = await User
|
|
171
|
+
.with('posts', 'profile')
|
|
172
|
+
.get();
|
|
173
|
+
|
|
174
|
+
// Ordonner et limiter
|
|
175
|
+
const recentUsers = await User
|
|
176
|
+
.orderBy('created_at', 'desc')
|
|
177
|
+
.limit(10)
|
|
178
|
+
.get();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Mettre à jour
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// Instance
|
|
185
|
+
const user = await User.find(1);
|
|
186
|
+
user.setAttribute('name', 'Updated Name');
|
|
187
|
+
await user.save();
|
|
188
|
+
|
|
189
|
+
// Bulk update
|
|
190
|
+
await User
|
|
191
|
+
.where('status', 'pending')
|
|
192
|
+
.update({ status: 'active' });
|
|
193
|
+
|
|
194
|
+
// One-liner façon Prisma (update + include)
|
|
195
|
+
const updated = await User
|
|
196
|
+
.where('id', 1)
|
|
197
|
+
.updateAndFetch({ name: 'Neo' }, ['profile', 'posts.comments']);
|
|
198
|
+
|
|
199
|
+
// Helpers par ID
|
|
200
|
+
const user1 = await User.updateAndFetchById(1, { name: 'Trinity' }, ['profile']);
|
|
201
|
+
await User.updateById(2, { status: 'active' });
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Supprimer
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
// Instance
|
|
208
|
+
const user = await User.find(1);
|
|
209
|
+
await user.destroy();
|
|
210
|
+
|
|
211
|
+
// Bulk delete
|
|
212
|
+
await User
|
|
213
|
+
.where('status', 'banned')
|
|
214
|
+
.delete();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Query Builder
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// Where clauses
|
|
221
|
+
const users = await User
|
|
222
|
+
.where('age', '>', 18)
|
|
223
|
+
.where('status', 'active')
|
|
224
|
+
.orWhere('role', 'admin')
|
|
225
|
+
.get();
|
|
226
|
+
|
|
227
|
+
// Where In
|
|
228
|
+
const users = await User
|
|
229
|
+
.whereIn('id', [1, 2, 3, 4, 5])
|
|
230
|
+
.get();
|
|
231
|
+
|
|
232
|
+
// Where Null
|
|
233
|
+
const users = await User
|
|
234
|
+
.whereNull('deleted_at')
|
|
235
|
+
.get();
|
|
236
|
+
|
|
237
|
+
// Where Not Null
|
|
238
|
+
const users = await User
|
|
239
|
+
.whereNotNull('email_verified_at')
|
|
240
|
+
.get();
|
|
241
|
+
|
|
242
|
+
// Pagination
|
|
243
|
+
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
|
+
// }
|
|
253
|
+
|
|
254
|
+
// Count
|
|
255
|
+
const count = await User.where('status', 'active').count();
|
|
256
|
+
|
|
257
|
+
// Joins
|
|
258
|
+
const result = await User
|
|
259
|
+
.join('profiles', 'users.id', 'profiles.user_id')
|
|
260
|
+
.leftJoin('countries', 'profiles.country_id', 'countries.id')
|
|
261
|
+
.whereLike('users.name', '%john%')
|
|
262
|
+
.whereBetween('users.age', [18, 65])
|
|
263
|
+
.select('users.*', 'profiles.bio', 'countries.name as country')
|
|
264
|
+
.orderBy('users.created_at', 'desc')
|
|
265
|
+
.get();
|
|
266
|
+
|
|
267
|
+
// Alias ergonomiques
|
|
268
|
+
const slim = await User
|
|
269
|
+
.columns(['id', 'name']) // alias de select(...)
|
|
270
|
+
.ordrer('created_at', 'desc') // alias typo de orderBy
|
|
271
|
+
.get();
|
|
272
|
+
|
|
273
|
+
// whereHas: filtrer les parents qui ont des enfants correspondants
|
|
274
|
+
// Exemple: Utilisateurs ayant au moins un post publié récemment
|
|
275
|
+
const authors = await User
|
|
276
|
+
.whereHas('posts', (q) => {
|
|
277
|
+
q.where('status', 'published').where('created_at', '>', new Date(Date.now() - 7*24*3600*1000));
|
|
278
|
+
})
|
|
279
|
+
.get();
|
|
280
|
+
|
|
281
|
+
// has: au moins N enfants
|
|
282
|
+
const prolific = await User.has('posts', '>=', 10).get();
|
|
283
|
+
|
|
284
|
+
// whereDoesntHave: aucun enfant
|
|
285
|
+
const orphans = await User.whereDoesntHave('posts').get();
|
|
286
|
+
|
|
287
|
+
// withCount: ajouter une colonne posts_count
|
|
288
|
+
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();
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Relations
|
|
299
|
+
|
|
300
|
+
#### One to One (hasOne)
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
class User extends Model {
|
|
304
|
+
profile() {
|
|
305
|
+
return this.hasOne(Profile, 'user_id');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const user = await User.find(1);
|
|
310
|
+
const profile = await user.profile().get();
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
#### One to Many (hasMany)
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
class User extends Model {
|
|
317
|
+
posts() {
|
|
318
|
+
return this.hasMany(Post, 'user_id');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const user = await User.find(1);
|
|
323
|
+
const posts = await user.posts().get();
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Belongs To (belongsTo)
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
class Post extends Model {
|
|
330
|
+
author() {
|
|
331
|
+
return this.belongsTo(User, 'user_id');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const post = await Post.find(1);
|
|
336
|
+
const author = await post.author().get();
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Many to Many (belongsToMany)
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
class User extends Model {
|
|
343
|
+
roles() {
|
|
344
|
+
return this.belongsToMany(
|
|
345
|
+
Role,
|
|
346
|
+
'user_roles', // Pivot table
|
|
347
|
+
'user_id', // Foreign key
|
|
348
|
+
'role_id' // Related key
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const user = await User.find(1);
|
|
354
|
+
const roles = await user.roles().get();
|
|
355
|
+
|
|
356
|
+
// belongsToMany helpers
|
|
357
|
+
await user.roles().attach([1, 2]);
|
|
358
|
+
await user.roles().detach(2);
|
|
359
|
+
await user.roles().sync([1, 3]);
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Has Many Through (hasManyThrough)
|
|
363
|
+
|
|
364
|
+
Permet d'accéder à une relation distante via un modèle intermédiaire (ex: User -> Post -> Comment pour récupérer les comments d'un user sans passer par les posts).
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
class User extends Model {
|
|
368
|
+
posts() {
|
|
369
|
+
return this.hasMany(Post, 'user_id');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
comments() {
|
|
373
|
+
// hasManyThrough(final, through, fkOnThrough?, throughKeyOnFinal?, localKey?, throughLocalKey?)
|
|
374
|
+
return this.hasManyThrough(Comment, Post, 'user_id', 'post_id');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const user = await User.find(1);
|
|
379
|
+
const comments = await user.comments().get();
|
|
380
|
+
|
|
381
|
+
// Eager load (avec contrainte):
|
|
382
|
+
const users = await User.with({ comments: q => q.where('created_at', '>', new Date(Date.now() - 7*24*3600*1000)) }).get();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Par défaut, les clés sont inférées selon les conventions:
|
|
386
|
+
|
|
387
|
+
- foreignKeyOnThrough: `${parentTableSingular}_id`
|
|
388
|
+
- throughKeyOnFinal: `${throughTableSingular}_id`
|
|
389
|
+
- localKey: clé primaire du parent (par défaut `id`)
|
|
390
|
+
- throughLocalKey: clé primaire du modèle intermédiaire (par défaut `id`)
|
|
391
|
+
|
|
392
|
+
### Eager Loading
|
|
393
|
+
|
|
394
|
+
```javascript
|
|
395
|
+
// Charger les relations avec les résultats
|
|
396
|
+
const users = await User.with('posts', 'profile').get();
|
|
397
|
+
|
|
398
|
+
// Accéder aux relations chargées
|
|
399
|
+
users.forEach(user => {
|
|
400
|
+
console.log(user.getAttribute('name'));
|
|
401
|
+
console.log(user.relations.posts);
|
|
402
|
+
console.log(user.relations.profile);
|
|
403
|
+
});
|
|
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
|
+
```
|
|
411
|
+
|
|
412
|
+
### Casts
|
|
413
|
+
|
|
414
|
+
Les casts permettent de convertir automatiquement les attributs:
|
|
415
|
+
|
|
416
|
+
```javascript
|
|
417
|
+
class User extends Model {
|
|
418
|
+
static casts = {
|
|
419
|
+
id: 'int',
|
|
420
|
+
age: 'integer',
|
|
421
|
+
balance: 'float',
|
|
422
|
+
email_verified: 'boolean',
|
|
423
|
+
metadata: 'json',
|
|
424
|
+
settings: 'array',
|
|
425
|
+
birthday: 'date'
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Attributs cachés
|
|
431
|
+
|
|
432
|
+
```javascript
|
|
433
|
+
class User extends Model {
|
|
434
|
+
static hidden = ['password', 'secret_token'];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const user = await User.find(1);
|
|
438
|
+
console.log(user.toJSON()); // password et secret_token ne sont pas inclus
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Timestamps
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
// Activer les timestamps automatiques (activé par défaut)
|
|
445
|
+
class User extends Model {
|
|
446
|
+
static timestamps = true; // created_at et updated_at
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Désactiver les timestamps
|
|
450
|
+
class Log extends Model {
|
|
451
|
+
static timestamps = false;
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## 🔧 Configuration avancée
|
|
456
|
+
|
|
457
|
+
### Connexions multiples
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
const mysqlDb = new DatabaseConnection({
|
|
461
|
+
driver: 'mysql',
|
|
462
|
+
host: 'localhost',
|
|
463
|
+
database: 'app_db',
|
|
464
|
+
user: 'root',
|
|
465
|
+
password: 'secret'
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const postgresDb = new DatabaseConnection({
|
|
469
|
+
driver: 'postgres',
|
|
470
|
+
host: 'localhost',
|
|
471
|
+
database: 'analytics_db',
|
|
472
|
+
user: 'postgres',
|
|
473
|
+
password: 'secret'
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Par modèle
|
|
477
|
+
class User extends Model {
|
|
478
|
+
static connection = mysqlDb;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
class Analytics extends Model {
|
|
482
|
+
static connection = postgresDb;
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Clé primaire personnalisée
|
|
487
|
+
|
|
488
|
+
```javascript
|
|
489
|
+
class User extends Model {
|
|
490
|
+
static primaryKey = 'user_id';
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Nom de table personnalisé
|
|
495
|
+
|
|
496
|
+
```javascript
|
|
497
|
+
class User extends Model {
|
|
498
|
+
static table = 'app_users';
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## 📝 API Reference
|
|
503
|
+
|
|
504
|
+
### DatabaseConnection
|
|
505
|
+
|
|
506
|
+
- `new DatabaseConnection(config?)` — lit automatiquement `.env` si `config` est omis
|
|
507
|
+
- `connect()` — établit la connexion (appelé automatiquement au besoin)
|
|
508
|
+
- `select(table, query)` — exécute un SELECT (utilisé par le Query Builder)
|
|
509
|
+
- `insert(table, data)` / `insertMany(table, data[])`
|
|
510
|
+
- `update(table, data, query)` / `delete(table, query)`
|
|
511
|
+
- `count(table, query)` — retourne le total
|
|
512
|
+
- `executeRawQuery(sql, params?)` — résultats normalisés (tableau d’objets)
|
|
513
|
+
- `execute(sql, params?)` — résultats natifs du driver (utile pour migrations)
|
|
514
|
+
- `increment(table, column, query, amount?)` — mise à jour atomique
|
|
515
|
+
- `decrement(table, column, query, amount?)`
|
|
516
|
+
- `close()` / `disconnect()` — fermer la connexion
|
|
517
|
+
|
|
518
|
+
### Model
|
|
519
|
+
|
|
520
|
+
- `static all()` - Récupérer tous les enregistrements
|
|
521
|
+
- `static find(id)` - Trouver par ID
|
|
522
|
+
- `static findOrFail(id)` - Trouver ou lancer une erreur
|
|
523
|
+
- `static where(column, operator, value)` - Ajouter une clause where
|
|
524
|
+
- `static create(attributes)` - Créer et sauvegarder
|
|
525
|
+
- `static insert(data)` - Insérer des données brutes
|
|
526
|
+
- `static update(attributes)` - Mise à jour bulk
|
|
527
|
+
- `static updateAndFetchById(id, attributes, relations?)` - Mise à jour par ID et retour du modèle (avec include)
|
|
528
|
+
- `static updateById(id, attributes)` - Mise à jour par ID
|
|
529
|
+
- `static delete()` - Suppression bulk
|
|
530
|
+
- `save()` - Sauvegarder l'instance
|
|
531
|
+
- `destroy()` - Supprimer l'instance
|
|
532
|
+
- `toJSON()` - Convertir en JSON
|
|
533
|
+
- `load(...relations)` - Charger des relations sur une instance, supporte la dot-notation
|
|
534
|
+
|
|
535
|
+
### QueryBuilder
|
|
536
|
+
|
|
537
|
+
- `select(...columns)` - Sélectionner des colonnes
|
|
538
|
+
- `where(column, operator, value)` - Clause WHERE
|
|
539
|
+
- `whereIn(column, values)` - Clause WHERE IN
|
|
540
|
+
- `whereNull(column)` - Clause WHERE NULL
|
|
541
|
+
- `whereNotNull(column)` - Clause WHERE NOT NULL
|
|
542
|
+
- `orWhere(column, operator, value)` - Clause OR WHERE
|
|
543
|
+
- `orderBy(column, direction)` - Ordonner les résultats
|
|
544
|
+
- `limit(value)` - Limiter les résultats
|
|
545
|
+
- `offset(value)` - Décaler les résultats
|
|
546
|
+
- `with(...relations)` - Eager loading
|
|
547
|
+
- `get()` - Exécuter et récupérer
|
|
548
|
+
- `first()` - Premier résultat
|
|
549
|
+
- `paginate(page, perPage)` - Paginer les résultats
|
|
550
|
+
- `count()` - Compter les résultats
|
|
551
|
+
- `exists()` - Vérifier l’existence
|
|
552
|
+
- `whereBetween(column, [min, max])` - Intervalle
|
|
553
|
+
- `whereLike(column, pattern)` - LIKE
|
|
554
|
+
- `whereHas(relation, cb?)` - Filtrer par relation (INNER JOIN)
|
|
555
|
+
- `has(relation, opOrCount, [count])` - Existence relationnelle (GROUP BY/HAVING)
|
|
556
|
+
- `whereDoesntHave(relation)` - Absence de relation (LEFT JOIN IS NULL)
|
|
557
|
+
- `join(table, first, [operator], second)` - INNER JOIN
|
|
558
|
+
- `leftJoin(table, first, [operator], second)` - LEFT JOIN
|
|
559
|
+
- `withCount(relations)` - Ajoute {relation}_count via sous-requête
|
|
560
|
+
- `distinct()` - SELECT DISTINCT
|
|
561
|
+
- `groupBy(...cols)` - GROUP BY
|
|
562
|
+
- `having(column, operator, value)` - HAVING
|
|
563
|
+
- `insert(data)` - Insérer des données (array => insertMany)
|
|
564
|
+
- `update(attributes)` - Mise à jour bulk
|
|
565
|
+
- `updateAndFetch(attributes, relations?)` - Mise à jour + premier enregistrement (avec include)
|
|
566
|
+
- `delete()` - Suppression bulk
|
|
567
|
+
- `increment(column, amount?)` - Incrément atomique
|
|
568
|
+
- `decrement(column, amount?)` - Décrément atomique
|
|
569
|
+
- `columns([...])` - Alias de `select(...cols)`
|
|
570
|
+
- `ordrer(column, direction?)` - Alias typo de `orderBy`
|
|
571
|
+
|
|
572
|
+
## 🛠️ Outils CLI
|
|
573
|
+
|
|
574
|
+
### 1. Initialisation d'un projet
|
|
575
|
+
|
|
576
|
+
```bash
|
|
577
|
+
outlet-init
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Crée un nouveau projet avec configuration de base de données, modèle exemple et fichier d'utilisation.
|
|
581
|
+
|
|
582
|
+
Depuis la version actuelle, outlet-init peut aussi générer un fichier `.env` avec les paramètres saisis (driver, hôte, port, utilisateur, mot de passe, base de données ou fichier SQLite). Si `.env` existe déjà, il n'est pas modifié.
|
|
583
|
+
|
|
584
|
+
Astuce: dans les environnements CI/tests, vous pouvez désactiver l'installation automatique du driver en définissant `OUTLET_INIT_NO_INSTALL=1`.
|
|
585
|
+
|
|
586
|
+
### 2. Système de Migrations
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
# Créer une migration
|
|
590
|
+
outlet-migrate make create_users_table
|
|
591
|
+
|
|
592
|
+
# 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
|
+
outlet-migrate migrate
|
|
618
|
+
|
|
619
|
+
# Voir le statut
|
|
620
|
+
outlet-migrate status
|
|
621
|
+
|
|
622
|
+
# Annuler la dernière migration
|
|
623
|
+
outlet-migrate rollback --steps 1
|
|
624
|
+
|
|
625
|
+
# Astuce: si le fichier database/config.js existe, il est prioritaire sur .env
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
**Fonctionnalités des Migrations :**
|
|
629
|
+
|
|
630
|
+
- ✅ **Création et gestion des migrations** (create, alter, drop tables)
|
|
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
|
|
640
|
+
|
|
641
|
+
**Documentation complète :**
|
|
642
|
+
|
|
643
|
+
- [MIGRATIONS.md](docs/MIGRATIONS.md) - Guide complet des migrations
|
|
644
|
+
|
|
645
|
+
### 3. Conversion SQL vers ORM
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
outlet-convert
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Convertit automatiquement des schémas SQL en modèles ORM :
|
|
652
|
+
|
|
653
|
+
#### Option 1 : Depuis un fichier SQL local
|
|
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 :**
|
|
665
|
+
|
|
666
|
+
- ✅ Détection automatique des types et casts
|
|
667
|
+
- ✅ **Génération automatique de TOUTES les relations** :
|
|
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
|
|
672
|
+
- ✅ Relations récursives (auto-relations)
|
|
673
|
+
- ✅ Détection des champs sensibles (password, token, etc.)
|
|
674
|
+
- ✅ Support des timestamps automatiques
|
|
675
|
+
- ✅ Conversion des noms de tables en classes PascalCase
|
|
676
|
+
|
|
677
|
+
### 4. Utilisation non-interactive (CI/CD)
|
|
678
|
+
|
|
679
|
+
Les commandes de migration supportent un mode non-interactif pratique pour l’automatisation:
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
# Exécuter les migrations en lisant la config depuis .env
|
|
683
|
+
outlet-migrate migrate
|
|
684
|
+
|
|
685
|
+
# Voir le statut
|
|
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
|
|
698
|
+
|
|
699
|
+
## 🤝 Contribution
|
|
700
|
+
|
|
701
|
+
Les contributions sont les bienvenues! N'hésitez pas à ouvrir une issue ou un pull request.
|
|
702
|
+
|
|
703
|
+
## 📄 Licence
|
|
704
|
+
|
|
705
|
+
MIT
|