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/bin/convert.js
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
|
|
7
|
+
const rl = readline.createInterface({
|
|
8
|
+
input: process.stdin,
|
|
9
|
+
output: process.stdout
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function question(query) {
|
|
13
|
+
return new Promise(resolve => rl.question(query, resolve));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Convertir un type SQL en type de cast JavaScript
|
|
17
|
+
function sqlTypeToCast(sqlType) {
|
|
18
|
+
const type = sqlType.toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (type.includes('int') || type.includes('serial')) return 'int';
|
|
21
|
+
if (type.includes('float') || type.includes('double') || type.includes('decimal') || type.includes('numeric')) return 'float';
|
|
22
|
+
if (type.includes('bool')) return 'boolean';
|
|
23
|
+
if (type.includes('json')) return 'json';
|
|
24
|
+
if (type.includes('date') || type.includes('time')) return 'date';
|
|
25
|
+
if (type.includes('text') || type.includes('char') || type.includes('varchar')) return 'string';
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parser une instruction CREATE TABLE
|
|
31
|
+
function parseCreateTable(sql) {
|
|
32
|
+
const tableMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?\s*\(/i);
|
|
33
|
+
if (!tableMatch) return null;
|
|
34
|
+
|
|
35
|
+
const tableName = tableMatch[1];
|
|
36
|
+
const columns = [];
|
|
37
|
+
const relations = [];
|
|
38
|
+
|
|
39
|
+
// Extraire les définitions de colonnes
|
|
40
|
+
const columnRegex = /`?(\w+)`?\s+(\w+(?:\(\d+(?:,\d+)?\))?)\s*([^,]*?)(?:[,)])/gi;
|
|
41
|
+
let match;
|
|
42
|
+
|
|
43
|
+
while ((match = columnRegex.exec(sql)) !== null) {
|
|
44
|
+
const [, columnName, columnType, constraints] = match;
|
|
45
|
+
|
|
46
|
+
// Ignorer les contraintes et index
|
|
47
|
+
if (columnName.toUpperCase() === 'PRIMARY' ||
|
|
48
|
+
columnName.toUpperCase() === 'UNIQUE' ||
|
|
49
|
+
columnName.toUpperCase() === 'KEY' ||
|
|
50
|
+
columnName.toUpperCase() === 'CONSTRAINT' ||
|
|
51
|
+
columnName.toUpperCase() === 'INDEX' ||
|
|
52
|
+
columnName.toUpperCase() === 'FOREIGN') {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const column = {
|
|
57
|
+
name: columnName,
|
|
58
|
+
type: columnType,
|
|
59
|
+
nullable: !constraints.toLowerCase().includes('not null'),
|
|
60
|
+
default: null,
|
|
61
|
+
autoIncrement: constraints.toLowerCase().includes('auto_increment'),
|
|
62
|
+
primary: constraints.toLowerCase().includes('primary key'),
|
|
63
|
+
unique: constraints.toLowerCase().includes('unique')
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Extraire la valeur par défaut
|
|
67
|
+
const defaultRegex = /DEFAULT\s+([^,\s]+)/i;
|
|
68
|
+
const defaultMatch = defaultRegex.exec(constraints);
|
|
69
|
+
if (defaultMatch) {
|
|
70
|
+
column.default = defaultMatch[1].replace(/['"]/g, '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
columns.push(column);
|
|
74
|
+
|
|
75
|
+
// Détecter les clés étrangères
|
|
76
|
+
if (columnName.endsWith('_id')) {
|
|
77
|
+
const relatedTable = columnName.replace(/_id$/, '') + 's';
|
|
78
|
+
relations.push({
|
|
79
|
+
type: 'belongsTo',
|
|
80
|
+
table: relatedTable,
|
|
81
|
+
foreignKey: columnName
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Détecter les clés étrangères explicites
|
|
87
|
+
const fkRegex = /FOREIGN\s+KEY\s*\(`?(\w+)`?\)\s*REFERENCES\s+`?(\w+)`?\s*\(`?(\w+)`?\)/gi;
|
|
88
|
+
while ((match = fkRegex.exec(sql)) !== null) {
|
|
89
|
+
const [, foreignKey, referencedTable, referencedColumn] = match;
|
|
90
|
+
|
|
91
|
+
const existingRelation = relations.find(r => r.foreignKey === foreignKey);
|
|
92
|
+
if (existingRelation) {
|
|
93
|
+
existingRelation.table = referencedTable;
|
|
94
|
+
existingRelation.relatedKey = referencedColumn;
|
|
95
|
+
} else {
|
|
96
|
+
relations.push({
|
|
97
|
+
type: 'belongsTo',
|
|
98
|
+
table: referencedTable,
|
|
99
|
+
foreignKey: foreignKey,
|
|
100
|
+
relatedKey: referencedColumn
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { tableName, columns, relations };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Analyser toutes les tables pour détecter les relations
|
|
109
|
+
function analyzeRelations(allTablesInfo) {
|
|
110
|
+
const relationshipMap = {};
|
|
111
|
+
|
|
112
|
+
// Initialiser la map pour chaque table
|
|
113
|
+
allTablesInfo.forEach(tableInfo => {
|
|
114
|
+
relationshipMap[tableInfo.tableName] = {
|
|
115
|
+
belongsTo: [],
|
|
116
|
+
hasMany: [],
|
|
117
|
+
hasOne: [],
|
|
118
|
+
belongsToMany: []
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Analyser les relations belongsTo et leurs inverses
|
|
123
|
+
allTablesInfo.forEach(tableInfo => {
|
|
124
|
+
const { tableName, columns, relations } = tableInfo;
|
|
125
|
+
|
|
126
|
+
relations.forEach(rel => {
|
|
127
|
+
if (rel.type === 'belongsTo') {
|
|
128
|
+
// Ajouter la relation belongsTo
|
|
129
|
+
relationshipMap[tableName].belongsTo.push(rel);
|
|
130
|
+
|
|
131
|
+
// Détecter la relation inverse (hasMany ou hasOne)
|
|
132
|
+
const foreignKey = rel.foreignKey;
|
|
133
|
+
const relatedTable = rel.table;
|
|
134
|
+
|
|
135
|
+
// Vérifier si c'est une relation hasOne (clé unique) ou hasMany
|
|
136
|
+
const foreignColumn = columns.find(col => col.name === foreignKey);
|
|
137
|
+
const isUnique = foreignColumn?.unique;
|
|
138
|
+
|
|
139
|
+
if (isUnique) {
|
|
140
|
+
// Relation hasOne inverse
|
|
141
|
+
relationshipMap[relatedTable].hasOne.push({
|
|
142
|
+
table: tableName,
|
|
143
|
+
foreignKey: foreignKey,
|
|
144
|
+
localKey: rel.relatedKey || 'id'
|
|
145
|
+
});
|
|
146
|
+
} else {
|
|
147
|
+
// Relation hasMany inverse
|
|
148
|
+
relationshipMap[relatedTable].hasMany.push({
|
|
149
|
+
table: tableName,
|
|
150
|
+
foreignKey: foreignKey,
|
|
151
|
+
localKey: rel.relatedKey || 'id'
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Détecter les tables pivot pour relations belongsToMany
|
|
159
|
+
allTablesInfo.forEach(tableInfo => {
|
|
160
|
+
const { tableName, columns } = tableInfo;
|
|
161
|
+
|
|
162
|
+
// Une table pivot typique a:
|
|
163
|
+
// - Pas de clé primaire auto-increment OU clé primaire composite
|
|
164
|
+
// - Exactement 2 clés étrangères
|
|
165
|
+
// - Peu ou pas d'autres colonnes (sauf timestamps)
|
|
166
|
+
const foreignKeys = columns.filter(col => col.name.endsWith('_id'));
|
|
167
|
+
const nonForeignNonTimestamp = columns.filter(col =>
|
|
168
|
+
!col.name.endsWith('_id') &&
|
|
169
|
+
col.name !== 'id' &&
|
|
170
|
+
col.name !== 'created_at' &&
|
|
171
|
+
col.name !== 'updated_at'
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const isPivotTable = foreignKeys.length === 2 && nonForeignNonTimestamp.length === 0;
|
|
175
|
+
|
|
176
|
+
if (isPivotTable) {
|
|
177
|
+
const [fk1, fk2] = foreignKeys;
|
|
178
|
+
const table1 = fk1.name.replace(/_id$/, '') + 's';
|
|
179
|
+
const table2 = fk2.name.replace(/_id$/, '') + 's';
|
|
180
|
+
|
|
181
|
+
// Ajouter la relation belongsToMany pour les deux tables
|
|
182
|
+
if (relationshipMap[table1] && relationshipMap[table2]) {
|
|
183
|
+
relationshipMap[table1].belongsToMany.push({
|
|
184
|
+
table: table2,
|
|
185
|
+
pivotTable: tableName,
|
|
186
|
+
foreignPivotKey: fk1.name,
|
|
187
|
+
relatedPivotKey: fk2.name
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
relationshipMap[table2].belongsToMany.push({
|
|
191
|
+
table: table1,
|
|
192
|
+
pivotTable: tableName,
|
|
193
|
+
foreignPivotKey: fk2.name,
|
|
194
|
+
relatedPivotKey: fk1.name
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return relationshipMap;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Générer le code du modèle
|
|
204
|
+
function generateModel(tableInfo, relationshipMap, options = {}) {
|
|
205
|
+
const { tableName, columns } = tableInfo;
|
|
206
|
+
|
|
207
|
+
// Nom de classe (PascalCase, singulier)
|
|
208
|
+
const className = tableName
|
|
209
|
+
.replace(/_/g, ' ')
|
|
210
|
+
.replace(/s$/, '')
|
|
211
|
+
.split(' ')
|
|
212
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
213
|
+
.join('');
|
|
214
|
+
|
|
215
|
+
// Colonnes fillable (exclure id, timestamps, clés étrangères si demandé)
|
|
216
|
+
const fillable = columns
|
|
217
|
+
.filter(col => {
|
|
218
|
+
if (col.name === 'id') return false;
|
|
219
|
+
if (col.name === 'created_at' || col.name === 'updated_at') return false;
|
|
220
|
+
if (options.excludeForeignKeys && col.name.endsWith('_id')) return false;
|
|
221
|
+
return true;
|
|
222
|
+
})
|
|
223
|
+
.map(col => col.name);
|
|
224
|
+
|
|
225
|
+
// Colonnes hidden (password, token, secret, etc.)
|
|
226
|
+
const hidden = columns
|
|
227
|
+
.filter(col => {
|
|
228
|
+
const name = col.name.toLowerCase();
|
|
229
|
+
return name.includes('password') ||
|
|
230
|
+
name.includes('token') ||
|
|
231
|
+
name.includes('secret') ||
|
|
232
|
+
name.includes('api_key');
|
|
233
|
+
})
|
|
234
|
+
.map(col => col.name);
|
|
235
|
+
|
|
236
|
+
// Casts
|
|
237
|
+
const casts = {};
|
|
238
|
+
columns.forEach(col => {
|
|
239
|
+
const cast = sqlTypeToCast(col.type);
|
|
240
|
+
if (cast && cast !== 'string') {
|
|
241
|
+
casts[col.name] = cast;
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Timestamps
|
|
246
|
+
const hasTimestamps = columns.some(col => col.name === 'created_at') &&
|
|
247
|
+
columns.some(col => col.name === 'updated_at');
|
|
248
|
+
|
|
249
|
+
// Clé primaire
|
|
250
|
+
const primaryKey = columns.find(col => col.primary)?.name || 'id';
|
|
251
|
+
|
|
252
|
+
// Générer le code
|
|
253
|
+
let code = `const { Model } = require('outlet-orm');\n\n`;
|
|
254
|
+
|
|
255
|
+
// Imports des modèles liés
|
|
256
|
+
const relatedModels = new Set();
|
|
257
|
+
|
|
258
|
+
// Obtenir toutes les relations pour cette table
|
|
259
|
+
const allRelations = relationshipMap[tableName] || {
|
|
260
|
+
belongsTo: [],
|
|
261
|
+
hasMany: [],
|
|
262
|
+
hasOne: [],
|
|
263
|
+
belongsToMany: []
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Collecter tous les modèles liés
|
|
267
|
+
[...allRelations.belongsTo, ...allRelations.hasMany, ...allRelations.hasOne, ...allRelations.belongsToMany].forEach(rel => {
|
|
268
|
+
const relatedClassName = rel.table
|
|
269
|
+
.replace(/_/g, ' ')
|
|
270
|
+
.replace(/s$/, '')
|
|
271
|
+
.split(' ')
|
|
272
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
273
|
+
.join('');
|
|
274
|
+
relatedModels.add(relatedClassName);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (relatedModels.size > 0) {
|
|
278
|
+
code += `// Importer les modèles liés\n`;
|
|
279
|
+
relatedModels.forEach(model => {
|
|
280
|
+
code += `// const ${model} = require('./${model}');\n`;
|
|
281
|
+
});
|
|
282
|
+
code += `\n`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
code += `class ${className} extends Model {\n`;
|
|
286
|
+
code += ` static table = '${tableName}';\n`;
|
|
287
|
+
|
|
288
|
+
if (primaryKey !== 'id') {
|
|
289
|
+
code += ` static primaryKey = '${primaryKey}';\n`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
code += ` static timestamps = ${hasTimestamps};\n`;
|
|
293
|
+
|
|
294
|
+
if (fillable.length > 0) {
|
|
295
|
+
code += ` static fillable = [\n`;
|
|
296
|
+
fillable.forEach((col, i) => {
|
|
297
|
+
code += ` '${col}'${i < fillable.length - 1 ? ',' : ''}\n`;
|
|
298
|
+
});
|
|
299
|
+
code += ` ];\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (hidden.length > 0) {
|
|
303
|
+
code += ` static hidden = [\n`;
|
|
304
|
+
hidden.forEach((col, i) => {
|
|
305
|
+
code += ` '${col}'${i < hidden.length - 1 ? ',' : ''}\n`;
|
|
306
|
+
});
|
|
307
|
+
code += ` ];\n`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (Object.keys(casts).length > 0) {
|
|
311
|
+
code += ` static casts = {\n`;
|
|
312
|
+
Object.entries(casts).forEach(([col, type], i, arr) => {
|
|
313
|
+
code += ` ${col}: '${type}'${i < arr.length - 1 ? ',' : ''}\n`;
|
|
314
|
+
});
|
|
315
|
+
code += ` };\n`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Relations
|
|
319
|
+
const hasRelations = allRelations.belongsTo.length > 0 ||
|
|
320
|
+
allRelations.hasMany.length > 0 ||
|
|
321
|
+
allRelations.hasOne.length > 0 ||
|
|
322
|
+
allRelations.belongsToMany.length > 0;
|
|
323
|
+
|
|
324
|
+
if (hasRelations) {
|
|
325
|
+
code += `\n // Relations\n`;
|
|
326
|
+
|
|
327
|
+
// Relations belongsTo
|
|
328
|
+
allRelations.belongsTo.forEach(rel => {
|
|
329
|
+
const relatedClassName = rel.table
|
|
330
|
+
.replace(/_/g, ' ')
|
|
331
|
+
.replace(/s$/, '')
|
|
332
|
+
.split(' ')
|
|
333
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
334
|
+
.join('');
|
|
335
|
+
|
|
336
|
+
const methodName = rel.table.replace(/_/g, '').replace(/s$/, '');
|
|
337
|
+
|
|
338
|
+
code += ` ${methodName}() {\n`;
|
|
339
|
+
code += ` return this.belongsTo(${relatedClassName}, '${rel.foreignKey}'`;
|
|
340
|
+
if (rel.relatedKey && rel.relatedKey !== 'id') {
|
|
341
|
+
code += `, '${rel.relatedKey}'`;
|
|
342
|
+
}
|
|
343
|
+
code += `);\n`;
|
|
344
|
+
code += ` }\n\n`;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Relations hasMany
|
|
348
|
+
allRelations.hasMany.forEach(rel => {
|
|
349
|
+
const relatedClassName = rel.table
|
|
350
|
+
.replace(/_/g, ' ')
|
|
351
|
+
.replace(/s$/, '')
|
|
352
|
+
.split(' ')
|
|
353
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
354
|
+
.join('');
|
|
355
|
+
|
|
356
|
+
const methodName = rel.table.replace(/_/g, '');
|
|
357
|
+
|
|
358
|
+
code += ` ${methodName}() {\n`;
|
|
359
|
+
code += ` return this.hasMany(${relatedClassName}, '${rel.foreignKey}'`;
|
|
360
|
+
if (rel.localKey && rel.localKey !== 'id') {
|
|
361
|
+
code += `, '${rel.localKey}'`;
|
|
362
|
+
}
|
|
363
|
+
code += `);\n`;
|
|
364
|
+
code += ` }\n\n`;
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Relations hasOne
|
|
368
|
+
allRelations.hasOne.forEach(rel => {
|
|
369
|
+
const relatedClassName = rel.table
|
|
370
|
+
.replace(/_/g, ' ')
|
|
371
|
+
.replace(/s$/, '')
|
|
372
|
+
.split(' ')
|
|
373
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
374
|
+
.join('');
|
|
375
|
+
|
|
376
|
+
const methodName = rel.table.replace(/_/g, '').replace(/s$/, '');
|
|
377
|
+
|
|
378
|
+
code += ` ${methodName}() {\n`;
|
|
379
|
+
code += ` return this.hasOne(${relatedClassName}, '${rel.foreignKey}'`;
|
|
380
|
+
if (rel.localKey && rel.localKey !== 'id') {
|
|
381
|
+
code += `, '${rel.localKey}'`;
|
|
382
|
+
}
|
|
383
|
+
code += `);\n`;
|
|
384
|
+
code += ` }\n\n`;
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Relations belongsToMany
|
|
388
|
+
allRelations.belongsToMany.forEach(rel => {
|
|
389
|
+
const relatedClassName = rel.table
|
|
390
|
+
.replace(/_/g, ' ')
|
|
391
|
+
.replace(/s$/, '')
|
|
392
|
+
.split(' ')
|
|
393
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
394
|
+
.join('');
|
|
395
|
+
|
|
396
|
+
const methodName = rel.table.replace(/_/g, '');
|
|
397
|
+
|
|
398
|
+
code += ` ${methodName}() {\n`;
|
|
399
|
+
code += ` return this.belongsToMany(${relatedClassName}, '${rel.pivotTable}', '${rel.foreignPivotKey}', '${rel.relatedPivotKey}');\n`;
|
|
400
|
+
code += ` }\n\n`;
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
code += `}\n\n`;
|
|
405
|
+
code += `module.exports = ${className};\n`;
|
|
406
|
+
|
|
407
|
+
return { className, code };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Convertir un fichier SQL
|
|
411
|
+
async function convertFromFile() {
|
|
412
|
+
const sqlFile = await question('Chemin du fichier SQL: ');
|
|
413
|
+
|
|
414
|
+
if (!fs.existsSync(sqlFile)) {
|
|
415
|
+
console.error(`❌ Fichier non trouvé: ${sqlFile}`);
|
|
416
|
+
rl.close();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const sqlContent = fs.readFileSync(sqlFile, 'utf8');
|
|
421
|
+
|
|
422
|
+
// Extraire toutes les instructions CREATE TABLE
|
|
423
|
+
const createTableRegex = /CREATE\s+TABLE\s+.*?;/gis;
|
|
424
|
+
const tables = sqlContent.match(createTableRegex) || [];
|
|
425
|
+
|
|
426
|
+
if (tables.length === 0) {
|
|
427
|
+
console.error('❌ Aucune instruction CREATE TABLE trouvée');
|
|
428
|
+
rl.close();
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.log(`\n✅ ${tables.length} table(s) trouvée(s)\n`);
|
|
433
|
+
|
|
434
|
+
const outputDir = await question('Dossier de sortie pour les modèles (défaut: ./models): ') || './models';
|
|
435
|
+
|
|
436
|
+
if (!fs.existsSync(outputDir)) {
|
|
437
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const excludeFk = await question('Exclure les clés étrangères de fillable? (o/N): ');
|
|
441
|
+
const options = {
|
|
442
|
+
excludeForeignKeys: excludeFk.toLowerCase() === 'o' || excludeFk.toLowerCase() === 'y'
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
// Parser toutes les tables
|
|
446
|
+
const allTablesInfo = [];
|
|
447
|
+
tables.forEach(sql => {
|
|
448
|
+
const tableInfo = parseCreateTable(sql);
|
|
449
|
+
if (tableInfo) {
|
|
450
|
+
allTablesInfo.push(tableInfo);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Analyser les relations entre toutes les tables
|
|
455
|
+
console.log('\n🔍 Analyse des relations...\n');
|
|
456
|
+
const relationshipMap = analyzeRelations(allTablesInfo);
|
|
457
|
+
|
|
458
|
+
// Générer les modèles avec toutes les relations
|
|
459
|
+
allTablesInfo.forEach(tableInfo => {
|
|
460
|
+
const { className, code } = generateModel(tableInfo, relationshipMap, options);
|
|
461
|
+
const filename = path.join(outputDir, `${className}.js`);
|
|
462
|
+
|
|
463
|
+
fs.writeFileSync(filename, code);
|
|
464
|
+
|
|
465
|
+
// Afficher les relations trouvées
|
|
466
|
+
const relations = relationshipMap[tableInfo.tableName];
|
|
467
|
+
const relationCount = relations.belongsTo.length + relations.hasMany.length +
|
|
468
|
+
relations.hasOne.length + relations.belongsToMany.length;
|
|
469
|
+
|
|
470
|
+
console.log(`✅ ${className}.js (${relationCount} relation${relationCount > 1 ? 's' : ''})`);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
console.log(`\n✨ Conversion terminée! ${allTablesInfo.length} modèle(s) créé(s) dans ${outputDir}\n`);
|
|
474
|
+
rl.close();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Récupérer la configuration de la base de données depuis l'utilisateur
|
|
478
|
+
async function getDatabaseConfig() {
|
|
479
|
+
const driver = await question('Driver (mysql/postgres/sqlite): ');
|
|
480
|
+
const dbConfig = { driver };
|
|
481
|
+
|
|
482
|
+
if (driver === 'mysql') {
|
|
483
|
+
dbConfig.host = await question('Host (défaut: localhost): ') || 'localhost';
|
|
484
|
+
dbConfig.port = await question('Port (défaut: 3306): ') || 3306;
|
|
485
|
+
dbConfig.database = await question('Database: ');
|
|
486
|
+
dbConfig.user = await question('User: ');
|
|
487
|
+
dbConfig.password = await question('Password: ');
|
|
488
|
+
} else if (driver === 'postgres' || driver === 'postgresql') {
|
|
489
|
+
dbConfig.host = await question('Host (défaut: localhost): ') || 'localhost';
|
|
490
|
+
dbConfig.port = await question('Port (défaut: 5432): ') || 5432;
|
|
491
|
+
dbConfig.database = await question('Database: ');
|
|
492
|
+
dbConfig.user = await question('User: ');
|
|
493
|
+
dbConfig.password = await question('Password: ');
|
|
494
|
+
} else if (driver === 'sqlite') {
|
|
495
|
+
dbConfig.filename = await question('Chemin du fichier SQLite: ');
|
|
496
|
+
} else {
|
|
497
|
+
throw new Error('Driver non supporté');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return dbConfig;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Récupérer la liste des tables depuis la base de données
|
|
504
|
+
async function fetchTablesList(connection, driver) {
|
|
505
|
+
let tables = [];
|
|
506
|
+
|
|
507
|
+
if (driver === 'mysql') {
|
|
508
|
+
const result = await connection.query('SHOW TABLES');
|
|
509
|
+
const key = Object.keys(result[0])[0];
|
|
510
|
+
tables = result.map(row => row[key]);
|
|
511
|
+
} else if (driver === 'postgres' || driver === 'postgresql') {
|
|
512
|
+
const result = await connection.query(
|
|
513
|
+
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
|
|
514
|
+
);
|
|
515
|
+
tables = result.map(row => row.table_name);
|
|
516
|
+
} else if (driver === 'sqlite') {
|
|
517
|
+
const result = await connection.query(
|
|
518
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
519
|
+
);
|
|
520
|
+
tables = result.map(row => row.name);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return tables;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Récupérer le CREATE TABLE pour une table donnée
|
|
527
|
+
async function fetchCreateTableSql(connection, driver, tableName) {
|
|
528
|
+
let createTableSql = '';
|
|
529
|
+
|
|
530
|
+
if (driver === 'mysql') {
|
|
531
|
+
const result = await connection.query(`SHOW CREATE TABLE \`${tableName}\``);
|
|
532
|
+
createTableSql = result[0]['Create Table'];
|
|
533
|
+
} else if (driver === 'postgres' || driver === 'postgresql') {
|
|
534
|
+
// Pour PostgreSQL, construire le CREATE TABLE depuis information_schema
|
|
535
|
+
const columns = await connection.query(`
|
|
536
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
537
|
+
FROM information_schema.columns
|
|
538
|
+
WHERE table_name = '${tableName}'
|
|
539
|
+
ORDER BY ordinal_position
|
|
540
|
+
`);
|
|
541
|
+
|
|
542
|
+
createTableSql = `CREATE TABLE ${tableName} (\n`;
|
|
543
|
+
columns.forEach((col, i) => {
|
|
544
|
+
createTableSql += ` ${col.column_name} ${col.data_type}`;
|
|
545
|
+
if (col.is_nullable === 'NO') createTableSql += ' NOT NULL';
|
|
546
|
+
if (col.column_default) createTableSql += ` DEFAULT ${col.column_default}`;
|
|
547
|
+
if (i < columns.length - 1) createTableSql += ',';
|
|
548
|
+
createTableSql += '\n';
|
|
549
|
+
});
|
|
550
|
+
createTableSql += ');';
|
|
551
|
+
} else if (driver === 'sqlite') {
|
|
552
|
+
const result = await connection.query(`SELECT sql FROM sqlite_master WHERE type='table' AND name='${tableName}'`);
|
|
553
|
+
createTableSql = result[0].sql;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return createTableSql;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Convertir depuis une base de données connectée
|
|
560
|
+
async function convertFromDatabase() {
|
|
561
|
+
console.log('\n📊 Conversion depuis une base de données\n');
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
// Récupérer la configuration
|
|
565
|
+
const dbConfig = await getDatabaseConfig();
|
|
566
|
+
|
|
567
|
+
// Connexion à la base de données
|
|
568
|
+
const { DatabaseConnection } = require('../src/index.js');
|
|
569
|
+
const connection = new DatabaseConnection(dbConfig);
|
|
570
|
+
|
|
571
|
+
console.log('\n🔄 Connexion à la base de données...');
|
|
572
|
+
await connection.connect();
|
|
573
|
+
console.log('✅ Connecté!\n');
|
|
574
|
+
|
|
575
|
+
// Récupérer la liste des tables
|
|
576
|
+
const tables = await fetchTablesList(connection, dbConfig.driver);
|
|
577
|
+
|
|
578
|
+
if (tables.length === 0) {
|
|
579
|
+
console.error('❌ Aucune table trouvée');
|
|
580
|
+
await connection.close();
|
|
581
|
+
rl.close();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
console.log(`✅ ${tables.length} table(s) trouvée(s):\n`);
|
|
586
|
+
tables.forEach((table, i) => console.log(` ${i + 1}. ${table}`));
|
|
587
|
+
console.log('');
|
|
588
|
+
|
|
589
|
+
const outputDir = await question('Dossier de sortie pour les modèles (défaut: ./models): ') || './models';
|
|
590
|
+
|
|
591
|
+
if (!fs.existsSync(outputDir)) {
|
|
592
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const excludeFk = await question('Exclure les clés étrangères de fillable? (o/N): ');
|
|
596
|
+
const options = {
|
|
597
|
+
excludeForeignKeys: excludeFk.toLowerCase() === 'o' || excludeFk.toLowerCase() === 'y'
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Parser toutes les tables
|
|
601
|
+
console.log('\n🔍 Récupération des schémas...\n');
|
|
602
|
+
const allTablesInfo = [];
|
|
603
|
+
|
|
604
|
+
// Pour chaque table, récupérer le CREATE TABLE
|
|
605
|
+
for (const tableName of tables) {
|
|
606
|
+
const createTableSql = await fetchCreateTableSql(connection, dbConfig.driver, tableName);
|
|
607
|
+
const tableInfo = parseCreateTable(createTableSql);
|
|
608
|
+
if (tableInfo) {
|
|
609
|
+
allTablesInfo.push(tableInfo);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Analyser les relations entre toutes les tables
|
|
614
|
+
console.log('🔍 Analyse des relations...\n');
|
|
615
|
+
const relationshipMap = analyzeRelations(allTablesInfo);
|
|
616
|
+
|
|
617
|
+
// Générer les modèles avec toutes les relations
|
|
618
|
+
allTablesInfo.forEach(tableInfo => {
|
|
619
|
+
const { className, code } = generateModel(tableInfo, relationshipMap, options);
|
|
620
|
+
const filename = path.join(outputDir, `${className}.js`);
|
|
621
|
+
|
|
622
|
+
fs.writeFileSync(filename, code);
|
|
623
|
+
|
|
624
|
+
// Afficher les relations trouvées
|
|
625
|
+
const relations = relationshipMap[tableInfo.tableName];
|
|
626
|
+
const relationCount = relations.belongsTo.length + relations.hasMany.length +
|
|
627
|
+
relations.hasOne.length + relations.belongsToMany.length;
|
|
628
|
+
|
|
629
|
+
console.log(`✅ ${className}.js (${relationCount} relation${relationCount > 1 ? 's' : ''})`);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
console.log(`\n✨ Conversion terminée! ${allTablesInfo.length} modèle(s) créé(s) dans ${outputDir}\n`);
|
|
633
|
+
|
|
634
|
+
await connection.close();
|
|
635
|
+
} catch (error) {
|
|
636
|
+
console.error('❌ Erreur:', error.message);
|
|
637
|
+
if (error.message === 'Driver non supporté') {
|
|
638
|
+
rl.close();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
rl.close();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Menu principal
|
|
647
|
+
async function main() {
|
|
648
|
+
console.log('\n╔═══════════════════════════════════════╗');
|
|
649
|
+
console.log('║ Outlet ORM - Convertisseur SQL ║');
|
|
650
|
+
console.log('╚═══════════════════════════════════════╝\n');
|
|
651
|
+
|
|
652
|
+
console.log('Choisissez une option:\n');
|
|
653
|
+
console.log(' 1. Convertir depuis un fichier SQL');
|
|
654
|
+
console.log(' 2. Convertir depuis une base de données connectée');
|
|
655
|
+
console.log(' 3. Quitter\n');
|
|
656
|
+
|
|
657
|
+
const choice = await question('Votre choix: ');
|
|
658
|
+
|
|
659
|
+
switch (choice) {
|
|
660
|
+
case '1':
|
|
661
|
+
await convertFromFile();
|
|
662
|
+
break;
|
|
663
|
+
case '2':
|
|
664
|
+
await convertFromDatabase();
|
|
665
|
+
break;
|
|
666
|
+
case '3':
|
|
667
|
+
console.log('Au revoir! 👋\n');
|
|
668
|
+
rl.close();
|
|
669
|
+
break;
|
|
670
|
+
default:
|
|
671
|
+
console.log('❌ Choix invalide\n');
|
|
672
|
+
rl.close();
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
main().catch(error => {
|
|
677
|
+
console.error('❌ Erreur:', error.message);
|
|
678
|
+
rl.close();
|
|
679
|
+
});
|