@mlagie/sql-connector 1.3.4 → 1.4.1

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.4",
3
+ "version": "1.4.1",
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": {
@@ -155,7 +155,6 @@ class Model {
155
155
  fs.writeFileSync(backupPath, insertSQL, 'utf-8');
156
156
  logs(`Sauvegarde SQL de la table '${dbTable}' effectuée dans '${backupPath}'.`);
157
157
  } else {
158
- fs.writeFileSync(backupPath, '', 'utf-8');
159
158
  logs(`Table '${dbTable}' vide, fichier '${backupPath}' créé.`);
160
159
  }
161
160
  } catch (err) {
@@ -180,213 +179,215 @@ class Model {
180
179
  throw err;
181
180
  }
182
181
 
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());
182
+ if (dangerousSync) {
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());
200
+ }
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}`);
200
211
  }
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
212
  }
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é.`);
226
+ }
227
+ rl.close();
228
+ resolve();
229
+ }
230
+ );
231
+ });
212
232
  }
213
233
 
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
+ // 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})`;
226
267
  }
227
- rl.close();
228
- resolve();
229
268
  }
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
269
 
263
- expectedTypeDef = sqlTypeMap[fieldType];
264
- if (sqlTypeMap[fieldType] === "VARCHAR" || sqlTypeMap[fieldType] === "INT") {
265
- const length = field.length > 0 ? field.length : 255;
266
- expectedTypeDef += `(${length})`;
267
- }
268
- }
270
+ // Normalisation pour la comparaison (minuscules, suppression des espaces)
271
+ const normalizeType = (typeStr) => {
272
+ return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
273
+ };
269
274
 
270
- // Normalisation pour la comparaison (minuscules, suppression des espaces)
271
- const normalizeType = (typeStr) => {
272
- return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
273
- };
275
+ const currentTypeNormalized = normalizeType(currentCol.Type);
276
+ const expectedTypeNormalized = normalizeType(expectedTypeDef);
274
277
 
275
- const currentTypeNormalized = normalizeType(currentCol.Type);
276
- const expectedTypeNormalized = normalizeType(expectedTypeDef);
278
+ if (currentTypeNormalized !== expectedTypeNormalized) {
279
+ logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
280
+ needModify = true;
281
+ }
277
282
 
278
- if (currentTypeNormalized !== expectedTypeNormalized) {
279
- logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
280
- needModify = true;
281
- }
283
+ // --- Vérification NULL/NOT NULL ---
284
+ const shouldBeNotNull = field.required === true || field.primary_key === true;
285
+ const isNullableInDB = currentCol.Null === "YES";
282
286
 
283
- // --- Vérification NULL/NOT NULL ---
284
- const shouldBeNotNull = field.required === true || field.primary_key === true;
285
- const isNullableInDB = currentCol.Null === "YES";
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
+ }
286
295
 
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
- }
296
+ // --- Vérification DEFAULT VALUE ---
297
+ const expectedDefault = field.default !== undefined ? field.default : null;
298
+ const currentDefault = currentCol.Default;
295
299
 
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}'`);
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`);
311
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}'`);
311
+ needModify = true;
312
+ }
312
313
  }
313
- }
314
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;
320
- }
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;
320
+ }
321
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;
327
- }
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;
327
+ }
328
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;
334
- }
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;
334
+ }
335
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}`);
342
- await conn.promise().query(alterSQL);
343
- logs(`Column ${fieldName} modified successfully`);
344
- } catch (err) {
345
- error(`Error modifying column ${fieldName}: ${err}`);
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}`);
342
+ await conn.promise().query(alterSQL);
343
+ logs(`Column ${fieldName} modified successfully`);
344
+ } catch (err) {
345
+ error(`Error modifying column ${fieldName}: ${err}`);
346
+ }
346
347
  }
347
348
  }
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
+ }
348
353
  }
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
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);
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}\` ${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
+ }
367
368
  }
368
- }
369
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);
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
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);
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
+ }
380
381
  }
381
- }
382
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}`);
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
+ }
390
391
  }
391
392
  }
392
393
  }
@@ -457,7 +458,7 @@ class Model {
457
458
  * @param {Object} [options.where] - Filtres (clé/valeur).
458
459
  * @param {Array} [options.order] - Ex: [['points', 'DESC']]
459
460
  * @param {number} [options.limit] - Limite de résultats.
460
- * @returns {Promise<Array<Object>>}
461
+ * @returns {Promise<Array<ModelInstance>>}
461
462
  */
462
463
  async findAll(options = {}) {
463
464
  const {
@@ -479,9 +480,12 @@ class Model {
479
480
  sql_request += ` LIMIT ${limit}`;
480
481
  }
481
482
 
483
+ logs(sql_request)
484
+
482
485
  return new Promise((resolve, reject) => {
483
486
  getConnexion().promise().query(sql_request).then(([rows]) => {
484
- resolve(rows);
487
+ const instances = rows.map(row => new ModelInstance(this.name, row, this.schema));
488
+ resolve(instances);
485
489
  }).catch((err) => {
486
490
  error(`Error executing findAll: ${err}`);
487
491
  resolve([]);