@mlagie/sql-connector 1.3.4 → 1.4.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 +7 -0
- package/package.json +1 -1
- package/src/models/Model.js +195 -193
package/README.md
CHANGED
|
@@ -111,6 +111,13 @@ const transferSchema = new Schema({
|
|
|
111
111
|
|
|
112
112
|
## Synchronisation automatique du schéma et gestion des migrations
|
|
113
113
|
|
|
114
|
+
> **⚠️ Important : Synchronisation du schéma et sécurité**
|
|
115
|
+
>
|
|
116
|
+
> Toutes les opérations de synchronisation avancée du schéma (modification de type, contraintes, renommage, suppression de colonnes, etc.) **ne sont effectuées que si vous activez explicitement l’option `{ dangerousSync: true }`** lors de l’appel à `Model.syncAllTables`.
|
|
117
|
+
>
|
|
118
|
+
> Par défaut (`dangerousSync: false`), aucune modification de structure n’est appliquée sur les tables existantes pour garantir la sécurité de vos données en production.
|
|
119
|
+
> Utilisez `dangerousSync` uniquement dans un environnement de développement ou lors de migrations contrôlées.
|
|
120
|
+
|
|
114
121
|
### Ajout, suppression et renommage de colonnes
|
|
115
122
|
|
|
116
123
|
- **Ajout automatique de colonnes**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Le module sql-connector permet de gérer les connexions à une base de données MySQL, de définir des schémas de tables, et d'interagir avec les données de manière simple et efficace.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
package/src/models/Model.js
CHANGED
|
@@ -180,215 +180,217 @@ class Model {
|
|
|
180
180
|
throw err;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
183
|
+
if (dangerousSync) {
|
|
184
|
+
// --- Restauration automatique si backup SQL trouvé ---
|
|
185
|
+
const backupPattern = `./backup_${model.name}_*.sql`;
|
|
186
|
+
const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
|
|
187
|
+
if (backupFiles.length > 0) {
|
|
188
|
+
const latestBackup = backupFiles.sort().reverse()[0];
|
|
189
|
+
const readline = require('readline');
|
|
190
|
+
const rl = readline.createInterface({
|
|
191
|
+
input: process.stdin,
|
|
192
|
+
output: process.stdout
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Demande restauration
|
|
196
|
+
const answer = await new Promise((resolve) => {
|
|
197
|
+
rl.question(
|
|
198
|
+
`Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
|
|
199
|
+
(answer) => {
|
|
200
|
+
resolve(answer.trim().toLowerCase());
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (answer === 'y') {
|
|
206
|
+
try {
|
|
207
|
+
const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
|
|
208
|
+
await conn.promise().query(sqlContent);
|
|
209
|
+
await logs(`Backup restauré pour la table '${model.name}'.`);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
|
|
200
212
|
}
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
if (answer === 'y') {
|
|
205
|
-
try {
|
|
206
|
-
const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
|
|
207
|
-
await conn.promise().query(sqlContent);
|
|
208
|
-
await logs(`Backup restauré pour la table '${model.name}'.`);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
|
|
211
213
|
}
|
|
214
|
+
|
|
215
|
+
// Toujours demander la suppression après restauration ou non
|
|
216
|
+
await new Promise((resolve) => {
|
|
217
|
+
rl.question(
|
|
218
|
+
`Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
|
|
219
|
+
(delAnswer) => {
|
|
220
|
+
if (delAnswer.trim().toLowerCase() === 'y') {
|
|
221
|
+
fs.unlinkSync(latestBackup);
|
|
222
|
+
logs(`Backup supprimé : ${latestBackup}`);
|
|
223
|
+
} else {
|
|
224
|
+
// Renomme le fichier pour ne plus proposer la restauration
|
|
225
|
+
fs.renameSync(latestBackup, latestBackup + '.ignored');
|
|
226
|
+
logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
|
|
227
|
+
}
|
|
228
|
+
rl.close();
|
|
229
|
+
resolve();
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
});
|
|
212
233
|
}
|
|
213
|
-
|
|
214
|
-
//
|
|
215
|
-
await
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
234
|
+
|
|
235
|
+
// 2. Synchronise les colonnes (renommage, ajout, suppression)
|
|
236
|
+
const [columns] = await conn.promise().query(
|
|
237
|
+
`SHOW COLUMNS FROM \`${model.name}\``
|
|
238
|
+
);
|
|
239
|
+
let existingCols = columns.map(col => col.Field);
|
|
240
|
+
|
|
241
|
+
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
242
|
+
if (existingCols.includes(fieldName)) {
|
|
243
|
+
// 1. Récupère infos colonne
|
|
244
|
+
const currentCol = columns.find(col => col.Field === fieldName);
|
|
245
|
+
const [indexes] = await conn.promise().query(
|
|
246
|
+
`SHOW INDEX FROM \`${model.name}\` WHERE Column_name = ?`, [fieldName]
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// 2. Vérifie les propriétés principales avec plus de précision
|
|
250
|
+
let needModify = false;
|
|
251
|
+
|
|
252
|
+
// --- Vérification du TYPE ---
|
|
253
|
+
const fieldType = getFieldType(field);
|
|
254
|
+
let expectedTypeDef;
|
|
255
|
+
|
|
256
|
+
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
257
|
+
// Pour les ENUM, on compare la définition complète
|
|
258
|
+
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
259
|
+
expectedTypeDef = `enum(${enumValues})`;
|
|
260
|
+
} else {
|
|
261
|
+
// Pour les autres types
|
|
262
|
+
if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
263
|
+
|
|
264
|
+
expectedTypeDef = sqlTypeMap[fieldType];
|
|
265
|
+
if (sqlTypeMap[fieldType] === "VARCHAR" || sqlTypeMap[fieldType] === "INT") {
|
|
266
|
+
const length = field.length > 0 ? field.length : 255;
|
|
267
|
+
expectedTypeDef += `(${length})`;
|
|
226
268
|
}
|
|
227
|
-
rl.close();
|
|
228
|
-
resolve();
|
|
229
269
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
// 1. Récupère infos colonne
|
|
243
|
-
const currentCol = columns.find(col => col.Field === fieldName);
|
|
244
|
-
const [indexes] = await conn.promise().query(
|
|
245
|
-
`SHOW INDEX FROM \`${model.name}\` WHERE Column_name = ?`, [fieldName]
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
// 2. Vérifie les propriétés principales avec plus de précision
|
|
249
|
-
let needModify = false;
|
|
250
|
-
|
|
251
|
-
// --- Vérification du TYPE ---
|
|
252
|
-
const fieldType = getFieldType(field);
|
|
253
|
-
let expectedTypeDef;
|
|
254
|
-
|
|
255
|
-
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
256
|
-
// Pour les ENUM, on compare la définition complète
|
|
257
|
-
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
258
|
-
expectedTypeDef = `enum(${enumValues})`;
|
|
259
|
-
} else {
|
|
260
|
-
// Pour les autres types
|
|
261
|
-
if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
262
|
-
|
|
263
|
-
expectedTypeDef = sqlTypeMap[fieldType];
|
|
264
|
-
if (sqlTypeMap[fieldType] === "VARCHAR" || sqlTypeMap[fieldType] === "INT") {
|
|
265
|
-
const length = field.length > 0 ? field.length : 255;
|
|
266
|
-
expectedTypeDef += `(${length})`;
|
|
270
|
+
|
|
271
|
+
// Normalisation pour la comparaison (minuscules, suppression des espaces)
|
|
272
|
+
const normalizeType = (typeStr) => {
|
|
273
|
+
return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const currentTypeNormalized = normalizeType(currentCol.Type);
|
|
277
|
+
const expectedTypeNormalized = normalizeType(expectedTypeDef);
|
|
278
|
+
|
|
279
|
+
if (currentTypeNormalized !== expectedTypeNormalized) {
|
|
280
|
+
logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
|
|
281
|
+
needModify = true;
|
|
267
282
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
283
|
+
|
|
284
|
+
// --- Vérification NULL/NOT NULL ---
|
|
285
|
+
const shouldBeNotNull = field.required === true || field.primary_key === true;
|
|
286
|
+
const isNullableInDB = currentCol.Null === "YES";
|
|
287
|
+
|
|
288
|
+
if (shouldBeNotNull && isNullableInDB) {
|
|
289
|
+
logs(`Nullability mismatch for ${fieldName}: DB allows NULL, expected NOT NULL`);
|
|
290
|
+
needModify = true;
|
|
291
|
+
}
|
|
292
|
+
if (!shouldBeNotNull && !isNullableInDB) {
|
|
293
|
+
logs(`Nullability mismatch for ${fieldName}: DB is NOT NULL, expected NULL allowed`);
|
|
294
|
+
needModify = true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// --- Vérification DEFAULT VALUE ---
|
|
298
|
+
const expectedDefault = field.default !== undefined ? field.default : null;
|
|
299
|
+
const currentDefault = currentCol.Default;
|
|
300
|
+
|
|
301
|
+
// Gestion spéciale pour les valeurs par défaut
|
|
302
|
+
if (expectedDefault !== null && currentDefault === null) {
|
|
303
|
+
logs(`Default value mismatch for ${fieldName}: DB has NULL, expected '${expectedDefault}'`);
|
|
304
|
+
needModify = true;
|
|
305
|
+
} else if (expectedDefault === null && currentDefault !== null) {
|
|
306
|
+
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected NULL`);
|
|
307
|
+
needModify = true;
|
|
308
|
+
} else if (expectedDefault !== null && currentDefault !== null) {
|
|
309
|
+
// Comparaison stringifiée pour éviter les problèmes de type
|
|
310
|
+
if (String(expectedDefault) !== String(currentDefault)) {
|
|
311
|
+
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected '${expectedDefault}'`);
|
|
312
|
+
needModify = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- Vérification UNIQUE constraint ---
|
|
317
|
+
const isUniqueInDB = indexes.some(idx => idx.Non_unique === 0 && idx.Key_name !== 'PRIMARY');
|
|
318
|
+
if (!!field.unique !== isUniqueInDB) {
|
|
319
|
+
logs(`Unique constraint mismatch for ${fieldName}: DB has unique=${isUniqueInDB}, expected=${!!field.unique}`);
|
|
320
|
+
needModify = true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Vérification PRIMARY KEY ---
|
|
324
|
+
const isPrimaryInDB = indexes.some(idx => idx.Key_name === 'PRIMARY');
|
|
325
|
+
if (!!field.primary_key !== isPrimaryInDB) {
|
|
326
|
+
logs(`Primary key mismatch for ${fieldName}: DB has PK=${isPrimaryInDB}, expected=${!!field.primary_key}`);
|
|
327
|
+
needModify = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- Vérification AUTO_INCREMENT ---
|
|
331
|
+
const isAutoIncrementInDB = currentCol.Extra.toLowerCase().includes('auto_increment');
|
|
332
|
+
if (!!field.auto_increment !== isAutoIncrementInDB) {
|
|
333
|
+
logs(`Auto increment mismatch for ${fieldName}: DB has AI=${isAutoIncrementInDB}, expected=${!!field.auto_increment}`);
|
|
311
334
|
needModify = true;
|
|
312
335
|
}
|
|
336
|
+
|
|
337
|
+
// 3. Si différence, on modifie
|
|
338
|
+
if (needModify) {
|
|
339
|
+
try {
|
|
340
|
+
const newColDef = getColumnDefinition(fieldName, field);
|
|
341
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` MODIFY COLUMN ${newColDef}`;
|
|
342
|
+
logs(`Modifying column ${fieldName}: ${alterSQL}`);
|
|
343
|
+
await conn.promise().query(alterSQL);
|
|
344
|
+
logs(`Column ${fieldName} modified successfully`);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
error(`Error modifying column ${fieldName}: ${err}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
313
349
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (!!field.unique !== isUniqueInDB) {
|
|
318
|
-
logs(`Unique constraint mismatch for ${fieldName}: DB has unique=${isUniqueInDB}, expected=${!!field.unique}`);
|
|
319
|
-
needModify = true;
|
|
350
|
+
// --- Warn si oldName est présent dans le schéma ---
|
|
351
|
+
if (field.oldName) {
|
|
352
|
+
logs(`⚠️ Pensez à retirer la propriété 'oldName' du champ '${fieldName}' dans le schéma JS de '${model.name}' pour éviter des renommages inutiles à l'avenir.`);
|
|
320
353
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --- Étape 1 : Renommage des colonnes ---
|
|
357
|
+
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
358
|
+
if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
|
|
359
|
+
// Génère la définition SQL de la nouvelle colonne
|
|
360
|
+
let colDef = getColumnDefinition(fieldName, field);
|
|
361
|
+
|
|
362
|
+
// Renomme la colonne
|
|
363
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` \`${fieldName}\` ${colDef}`;
|
|
364
|
+
await conn.promise().query(alterSQL);
|
|
365
|
+
logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
|
|
366
|
+
// Mets à jour existingCols pour la suite
|
|
367
|
+
existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
|
|
327
368
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// --- Étape 2 : Ajout des colonnes manquantes ---
|
|
372
|
+
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
373
|
+
if (!existingCols.includes(fieldName)) {
|
|
374
|
+
let colDef = getColumnDefinition(fieldName, field);
|
|
375
|
+
|
|
376
|
+
// Ajoute la colonne
|
|
377
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN ${colDef}`;
|
|
378
|
+
await conn.promise().query(alterSQL);
|
|
379
|
+
logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
|
|
380
|
+
existingCols.push(fieldName);
|
|
334
381
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
|
|
385
|
+
if (dangerousSync) {
|
|
386
|
+
for (const col of existingCols) {
|
|
387
|
+
if (!Object.keys(model.schema.schemaDict).includes(col)) {
|
|
388
|
+
const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
|
|
342
389
|
await conn.promise().query(alterSQL);
|
|
343
|
-
logs(`
|
|
344
|
-
} catch (err) {
|
|
345
|
-
error(`Error modifying column ${fieldName}: ${err}`);
|
|
390
|
+
logs(`Colonne ${col} supprimée de ${model.name}`);
|
|
346
391
|
}
|
|
347
392
|
}
|
|
348
393
|
}
|
|
349
|
-
// --- Warn si oldName est présent dans le schéma ---
|
|
350
|
-
if (field.oldName) {
|
|
351
|
-
logs(`⚠️ Pensez à retirer la propriété 'oldName' du champ '${fieldName}' dans le schéma JS de '${model.name}' pour éviter des renommages inutiles à l'avenir.`);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// --- Étape 1 : Renommage des colonnes ---
|
|
356
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
357
|
-
if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
|
|
358
|
-
// Génère la définition SQL de la nouvelle colonne
|
|
359
|
-
let colDef = getColumnDefinition(fieldName, field);
|
|
360
|
-
|
|
361
|
-
// Renomme la colonne
|
|
362
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` \`${fieldName}\` ${colDef}`;
|
|
363
|
-
await conn.promise().query(alterSQL);
|
|
364
|
-
logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
|
|
365
|
-
// Mets à jour existingCols pour la suite
|
|
366
|
-
existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
// --- Étape 2 : Ajout des colonnes manquantes ---
|
|
371
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
372
|
-
if (!existingCols.includes(fieldName)) {
|
|
373
|
-
let colDef = getColumnDefinition(fieldName, field);
|
|
374
|
-
|
|
375
|
-
// Ajoute la colonne
|
|
376
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN ${colDef}`;
|
|
377
|
-
await conn.promise().query(alterSQL);
|
|
378
|
-
logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
|
|
379
|
-
existingCols.push(fieldName);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
|
|
384
|
-
if (dangerousSync) {
|
|
385
|
-
for (const col of existingCols) {
|
|
386
|
-
if (!Object.keys(model.schema.schemaDict).includes(col)) {
|
|
387
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
|
|
388
|
-
await conn.promise().query(alterSQL);
|
|
389
|
-
logs(`Colonne ${col} supprimée de ${model.name}`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
394
|
}
|
|
393
395
|
}
|
|
394
396
|
// Vide la liste d'attente
|