@mlagie/sql-connector 1.3.3 → 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 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.3",
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": {
@@ -180,215 +180,217 @@ class Model {
180
180
  throw err;
181
181
  }
182
182
 
183
- // --- Restauration automatique si backup SQL trouvé ---
184
- const backupPattern = `./backup_${model.name}_*.sql`;
185
- const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
186
- if (backupFiles.length > 0) {
187
- const latestBackup = backupFiles.sort().reverse()[0];
188
- const readline = require('readline');
189
- const rl = readline.createInterface({
190
- input: process.stdin,
191
- output: process.stdout
192
- });
193
-
194
- // Demande restauration
195
- const answer = await new Promise((resolve) => {
196
- rl.question(
197
- `Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
198
- (answer) => {
199
- resolve(answer.trim().toLowerCase());
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
- // Toujours demander la suppression après restauration ou non
215
- await new Promise((resolve) => {
216
- rl.question(
217
- `Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
218
- (delAnswer) => {
219
- if (delAnswer.trim().toLowerCase() === 'y') {
220
- fs.unlinkSync(latestBackup);
221
- logs(`Backup supprimé : ${latestBackup}`);
222
- } else {
223
- // Renomme le fichier pour ne plus proposer la restauration
224
- fs.renameSync(latestBackup, latestBackup + '.ignored');
225
- logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
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
- // 2. Synchronise les colonnes (renommage, ajout, suppression)
235
- const [columns] = await conn.promise().query(
236
- `SHOW COLUMNS FROM \`${model.name}\``
237
- );
238
- let existingCols = columns.map(col => col.Field);
239
-
240
- for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
241
- if (existingCols.includes(fieldName)) {
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
- // Normalisation pour la comparaison (minuscules, suppression des espaces)
271
- const normalizeType = (typeStr) => {
272
- return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
273
- };
274
-
275
- const currentTypeNormalized = normalizeType(currentCol.Type);
276
- const expectedTypeNormalized = normalizeType(expectedTypeDef);
277
-
278
- if (currentTypeNormalized !== expectedTypeNormalized) {
279
- logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
280
- needModify = true;
281
- }
282
-
283
- // --- Vérification NULL/NOT NULL ---
284
- const shouldBeNotNull = field.required === true || field.primary_key === true;
285
- const isNullableInDB = currentCol.Null === "YES";
286
-
287
- if (shouldBeNotNull && isNullableInDB) {
288
- logs(`Nullability mismatch for ${fieldName}: DB allows NULL, expected NOT NULL`);
289
- needModify = true;
290
- }
291
- if (!shouldBeNotNull && !isNullableInDB) {
292
- logs(`Nullability mismatch for ${fieldName}: DB is NOT NULL, expected NULL allowed`);
293
- needModify = true;
294
- }
295
-
296
- // --- Vérification DEFAULT VALUE ---
297
- const expectedDefault = field.default !== undefined ? field.default : null;
298
- const currentDefault = currentCol.Default;
299
-
300
- // Gestion spéciale pour les valeurs par défaut
301
- if (expectedDefault !== null && currentDefault === null) {
302
- logs(`Default value mismatch for ${fieldName}: DB has NULL, expected '${expectedDefault}'`);
303
- needModify = true;
304
- } else if (expectedDefault === null && currentDefault !== null) {
305
- logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected NULL`);
306
- needModify = true;
307
- } else if (expectedDefault !== null && currentDefault !== null) {
308
- // Comparaison stringifiée pour éviter les problèmes de type
309
- if (String(expectedDefault) !== String(currentDefault)) {
310
- logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected '${expectedDefault}'`);
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
- // --- Vérification UNIQUE constraint ---
316
- const isUniqueInDB = indexes.some(idx => idx.Non_unique === 0 && idx.Key_name !== 'PRIMARY');
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
- // --- Vérification PRIMARY KEY ---
323
- const isPrimaryInDB = indexes.some(idx => idx.Key_name === 'PRIMARY');
324
- if (!!field.primary_key !== isPrimaryInDB) {
325
- logs(`Primary key mismatch for ${fieldName}: DB has PK=${isPrimaryInDB}, expected=${!!field.primary_key}`);
326
- needModify = true;
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
- // --- Vérification AUTO_INCREMENT ---
330
- const isAutoIncrementInDB = currentCol.Extra.toLowerCase().includes('auto_increment');
331
- if (!!field.auto_increment !== isAutoIncrementInDB) {
332
- logs(`Auto increment mismatch for ${fieldName}: DB has AI=${isAutoIncrementInDB}, expected=${!!field.auto_increment}`);
333
- needModify = true;
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
- // 3. Si différence, on modifie
337
- if (needModify) {
338
- try {
339
- const newColDef = getColumnDefinition(fieldName, field);
340
- const alterSQL = `ALTER TABLE \`${model.name}\` MODIFY COLUMN ${newColDef}`;
341
- logs(`Modifying column ${fieldName}: ${alterSQL}`);
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(`Column ${fieldName} modified successfully`);
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
@@ -1,3 +1,4 @@
1
+ const { error } = require("@mlagie/logger");
1
2
  const { getConnexion } = require("../db/connexion");
2
3
  const formatObject = require("../utils/formatObject");
3
4
  const generateCondition = require("../utils/generateCondition");