@mlagie/sql-connector 1.1.4-beta → 1.2.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
@@ -1,3 +1,7 @@
1
+ # sql-connector
2
+ <img src="https://api.visitorbadge.io/api/VisitorHit?user=lagie-marin&repo=sql-connector-badge&countColor=%237B1E7A" height="20px"/> ![GitHub package.json version](https://img.shields.io/github/package-json/v/lagie-marin/sql-connector?color=#008000) ![NPM Downloads](https://img.shields.io/npm/d18m/%40mlagie%2Fsql-connector?color=#008000) ![NPM Downloads](https://img.shields.io/npm/dw/%40mlagie%2Fsql-connector?color=#008000) ![GitHub followers](https://img.shields.io/github/followers/lagie-marin?style=plastic&color=color%3D%23008000) ![GitHub repo size](https://img.shields.io/github/repo-size/lagie-marin/sql-connector?color=%green)
3
+ ![GitHub last commit](https://img.shields.io/github/last-commit/lagie-marin/sql-connector) ![GitHub forks](https://img.shields.io/github/forks/lagie-marin/sql-connector?style=plastic&color=%green)
4
+
1
5
  # Documentation du module `sql-connector`
2
6
 
3
7
  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.
@@ -104,6 +108,59 @@ const transferSchema = new Schema({
104
108
  }
105
109
  });
106
110
  ```
111
+
112
+ ## Synchronisation automatique du schéma et gestion des migrations
113
+
114
+ ### Ajout, suppression et renommage de colonnes
115
+
116
+ - **Ajout automatique de colonnes**
117
+ Lorsque vous ajoutez un champ dans le schéma JS, la colonne correspondante est automatiquement ajoutée dans la base de données lors de la synchronisation avec :
118
+ ```js
119
+ await Model.syncAllTables();
120
+ ```
121
+
122
+ - **Suppression automatique de colonnes**
123
+ Si vous retirez un champ du schéma JS, la colonne reste dans la base par défaut.
124
+ Pour supprimer automatiquement les colonnes disparues, utilisez :
125
+ ```js
126
+ await Model.syncAllTables({ dangerousSync: true });
127
+ ```
128
+ ⚠️ Attention, cela supprime les données de ces colonnes.
129
+
130
+ - **Renommage de colonne sans perte de données**
131
+ Pour renommer une colonne, ajoutez la propriété `oldName` dans le schéma :
132
+ ```js
133
+ const userSchema = new Schema({
134
+ role: { type: String, oldName: "rang" }
135
+ });
136
+ ```
137
+ Lors de la synchronisation, la colonne SQL sera renommée sans perte de données.
138
+ Un avertissement s’affichera pour vous rappeler de retirer `oldName` du schéma après migration.
139
+
140
+ ### Suppression et sauvegarde des tables orphelines
141
+
142
+ - Si une table SQL n’a plus de schéma JS associé, elle est supprimée automatiquement lors de la synchronisation.
143
+ - **Avant suppression**, un fichier de backup SQL (INSERTs) est généré dans le dossier courant (ex : `backup_MaTable_1690000000000.sql`).
144
+
145
+ ### Restauration et gestion des backups
146
+
147
+ - Si une table supprimée réapparaît dans le schéma, le module détecte la présence d’un backup et propose :
148
+ 1. **De restaurer les données** (exécution du fichier SQL).
149
+ 2. **De supprimer le backup** après restauration ou non.
150
+ 3. Si vous refusez la suppression, le backup est renommé en `.ignored` et ne sera plus proposé.
151
+
152
+ #### Exemple d’utilisation
153
+
154
+ ```js
155
+ // Synchronisation simple (ajout/renommage de colonnes, suppression de tables orphelines avec backup)
156
+ await Model.syncAllTables();
157
+
158
+ // Synchronisation avec suppression automatique des colonnes disparues
159
+ await Model.syncAllTables({ dangerousSync: true });
160
+ ```
161
+
162
+ ---
163
+
107
164
  ## Class Model
108
165
  Représente un modèle de base de données.
109
166
  * ### Constructeur :
package/index.d.ts CHANGED
@@ -98,7 +98,7 @@ export class Model {
98
98
  * Crée toutes les tables dans l'ordre correct en fonction des foreign keys.
99
99
  * @returns {Promise<void>}
100
100
  */
101
- static createAllTables(): Promise<void>;
101
+ static syncAllTables({ dangerousSync = false } = {}): Promise<void>;
102
102
  /**
103
103
  * Saves data to the database table.
104
104
  * @param {Object} data The data to insert into the table.
package/index.js CHANGED
@@ -4,6 +4,7 @@ const { connect, logout } = require("./src/db/connect");
4
4
  const {Schema} = require("./src/models/Schema");
5
5
  const { sqlType } = require("./src/utils/sqlType");
6
6
  const { sqlTypeMap } = require("./src/utils/sqlTypeMap");
7
- const client = require("./client");
7
+
8
+ let client = {};
8
9
 
9
10
  module.exports = { client, connect, logout, Schema, Model, ModelInstance, sqlType, sqlTypeMap }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mlagie/sql-connector",
3
- "version": "1.1.4-beta",
3
+ "version": "1.2.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": {
@@ -10,7 +10,10 @@
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/lagie-marin/sql-connector.git"
12
12
  },
13
- "keywords": ["sql", "schema"],
13
+ "keywords": [
14
+ "sql",
15
+ "schema"
16
+ ],
14
17
  "author": "lagie-marin",
15
18
  "license": "MIT",
16
19
  "type": "commonjs",
@@ -21,6 +24,7 @@
21
24
  "private": false,
22
25
  "dependencies": {
23
26
  "@mlagie/logger": "1.0.1",
24
- "mysql2": "^3.14.4"
27
+ "glob": "^11.0.3",
28
+ "mysql2": "3.14.5"
25
29
  }
26
30
  }
@@ -1,6 +1,7 @@
1
1
  const { logs, error, sql } = require("@mlagie/logger");
2
2
  const { sqlTypeMap } = require("../utils/sqlTypeMap");
3
3
  const { getConnexion } = require("../db/connexion");
4
+ const generateCondition = require("../utils/generateCondition");
4
5
 
5
6
  function getFieldType(field) {
6
7
  if (typeof field === "object") {
@@ -45,6 +46,27 @@ function ifReservedKeywords(tableName) {
45
46
  return false;
46
47
  }
47
48
 
49
+ function getColumnDefinition(fieldName, field) {
50
+ const fieldType = getFieldType(field);
51
+ let colDef = "";
52
+
53
+ if (Array.isArray(field.enum) && field.enum.length > 0) {
54
+ const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
55
+ colDef = `ENUM(${enumValues})`;
56
+ } else {
57
+ if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
58
+ colDef = `${sqlTypeMap[fieldType]}${(sqlTypeMap[fieldType] == "VARCHAR" || sqlTypeMap[fieldType] == "INT") ? `(${field.length > 0 ? field.length : 255})` : ""}`;
59
+ }
60
+ if (field.required) colDef += ' NOT NULL';
61
+ if (field.default !== undefined && field.default != null) colDef += ` DEFAULT "${field.default}"`;
62
+ if (field.default === null) colDef += ` DEFAULT NULL`;
63
+ if (field.unique) colDef += ' UNIQUE';
64
+ if (field.auto_increment) colDef += ' AUTO_INCREMENT';
65
+ if (field.primary_key) colDef += ' PRIMARY KEY';
66
+ if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
67
+ return colDef;
68
+ }
69
+
48
70
  /**
49
71
  * Represents a database model.
50
72
  * @class
@@ -66,25 +88,33 @@ class Model {
66
88
  }
67
89
 
68
90
  /**
69
- * Crée toutes les tables dans l'ordre correct en fonction des foreign keys.
91
+ * Synchronise toutes les tables avec leur schéma JS (création + ajout des colonnes manquantes).
70
92
  * @returns {Promise<void>}
71
93
  */
72
- static async createAllTables() {
94
+ static async syncAllTables({ dangerousSync = false } = {}) {
95
+ const fs = require('fs');
96
+ const path = require('path');
97
+ const glob = require('glob');
98
+
73
99
  // Dépendances : {table: [tables dont elle dépend]}
74
100
  const dependencies = {};
75
101
  const modelMap = {};
76
102
  for (const model of Model.pendingModels) {
77
103
  modelMap[model.name] = model;
78
104
  dependencies[model.name] = [];
79
- for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
105
+ for (const [_, field] of Object.entries(model.schema.schemaDict)) {
80
106
  if (field && field.foreignKey) {
81
- // field.foreignKey peut être "autreTable(colonne)"
82
107
  const refTable = field.foreignKey.split('(')[0].trim();
83
108
  dependencies[model.name].push(refTable);
84
109
  }
85
110
  }
86
111
  }
87
112
 
113
+ // Récupère toutes les tables existantes dans la base
114
+ const conn = getConnexion();
115
+ const [dbTablesRows] = await conn.promise().query("SHOW TABLES");
116
+ const dbTables = dbTablesRows.map(row => Object.values(row)[0]);
117
+
88
118
  // Tri topologique
89
119
  const sorted = [];
90
120
  const visited = {};
@@ -102,19 +132,151 @@ class Model {
102
132
  if (!visited[table]) visit(table);
103
133
  }
104
134
 
105
- // Création des tables dans l'ordre
135
+ // --- Détruit les tables qui n'ont plus de schéma ---
136
+ for (const dbTable of dbTables) {
137
+ if (!modelMap[dbTable]) {
138
+ try {
139
+ const backupPath = `./backup_${dbTable}_${Date.now()}.sql`;
140
+ // Sauvegarde rapide en SQL (INSERTs)
141
+ const [rows] = await conn.promise().query(`SELECT * FROM \`${dbTable}\``);
142
+ if (rows.length > 0) {
143
+ const columns = Object.keys(rows[0]).map(col => `\`${col}\``).join(', ');
144
+ const values = rows.map(row =>
145
+ '(' + Object.values(row).map(val =>
146
+ val === null ? 'NULL' : conn.escape(val)
147
+ ).join(', ') + ')'
148
+ ).join(',\n');
149
+ const insertSQL = `INSERT INTO \`${dbTable}\` (${columns}) VALUES${values};\n`;
150
+ fs.writeFileSync(backupPath, insertSQL, 'utf-8');
151
+ logs(`Sauvegarde SQL de la table '${dbTable}' effectuée dans '${backupPath}'.`);
152
+ } else {
153
+ fs.writeFileSync(backupPath, '', 'utf-8');
154
+ logs(`Table '${dbTable}' vide, fichier '${backupPath}' créé.`);
155
+ }
156
+ } catch (err) {
157
+ error(`Erreur lors de la sauvegarde SQL de la table '${dbTable}': ${err}`);
158
+ }
159
+ logs(`Table '${dbTable}' n'a plus de schéma associé, suppression...`);
160
+ await conn.promise().query(`DROP TABLE IF EXISTS \`${dbTable}\``);
161
+ logs(`Table '${dbTable}' supprimée.`);
162
+ }
163
+ }
164
+
165
+ // Création/synchronisation des tables dans l'ordre
106
166
  for (const table of sorted) {
107
167
  const model = modelMap[table];
108
- await new Promise((resolve, reject) => {
109
- getConnexion().query(model.generateCreateTableStatement(model.schema.schemaDict), (err) => {
110
- if (err) {
111
- error(`Error creating table: ${err} with table name: ${model.name}`);
112
- return reject(err);
168
+
169
+ // 1. Crée la table si elle n'existe pas (version avec promesses)
170
+ try {
171
+ await conn.promise().query(model.generateCreateTableStatement(model.schema.schemaDict));
172
+ await logs(`La table ${model.name} a été créée ou existe déjà`);
173
+ } catch (err) {
174
+ error(`Error creating table: ${err} with table name: ${model.name}`);
175
+ throw err;
176
+ }
177
+
178
+ // --- Restauration automatique si backup SQL trouvé ---
179
+ const backupPattern = `./backup_${model.name}_*.sql`;
180
+ const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
181
+ if (backupFiles.length > 0) {
182
+ const latestBackup = backupFiles.sort().reverse()[0];
183
+ const readline = require('readline');
184
+ const rl = readline.createInterface({
185
+ input: process.stdin,
186
+ output: process.stdout
187
+ });
188
+
189
+ // Demande restauration
190
+ const answer = await new Promise((resolve) => {
191
+ rl.question(
192
+ `Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
193
+ (answer) => {
194
+ resolve(answer.trim().toLowerCase());
195
+ }
196
+ );
197
+ });
198
+
199
+ if (answer === 'y') {
200
+ try {
201
+ const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
202
+ await conn.promise().query(sqlContent);
203
+ await logs(`Backup restauré pour la table '${model.name}'.`);
204
+ } catch (err) {
205
+ error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
113
206
  }
114
- logs(`La table ${model.name} a été créé`);
115
- resolve();
207
+ }
208
+
209
+ // Toujours demander la suppression après restauration ou non
210
+ await new Promise((resolve) => {
211
+ rl.question(
212
+ `Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
213
+ (delAnswer) => {
214
+ if (delAnswer.trim().toLowerCase() === 'y') {
215
+ fs.unlinkSync(latestBackup);
216
+ logs(`Backup supprimé : ${latestBackup}`);
217
+ } else {
218
+ // Renomme le fichier pour ne plus proposer la restauration
219
+ fs.renameSync(latestBackup, latestBackup + '.ignored');
220
+ logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
221
+ }
222
+ rl.close();
223
+ resolve();
224
+ }
225
+ );
116
226
  });
117
- });
227
+ }
228
+
229
+ // 2. Synchronise les colonnes (renommage, ajout, suppression)
230
+ const [columns] = await conn.promise().query(
231
+ `SHOW COLUMNS FROM \`${model.name}\``
232
+ );
233
+ let existingCols = columns.map(col => col.Field);
234
+
235
+ // --- Warn si oldName est présent dans le schéma ---
236
+ for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
237
+ if (field.oldName) {
238
+ 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.`);
239
+ }
240
+ }
241
+
242
+ // --- Étape 1 : Renommage des colonnes ---
243
+ for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
244
+ if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
245
+ // Génère la définition SQL de la nouvelle colonne
246
+ let colDef = getColumnDefinition(fieldName, field);
247
+
248
+ // Renomme la colonne
249
+ const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` \`${fieldName}\` ${colDef}`;
250
+ await conn.promise().query(alterSQL);
251
+ logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
252
+ // Mets à jour existingCols pour la suite
253
+ existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
254
+ }
255
+ }
256
+
257
+ // --- Étape 2 : Ajout des colonnes manquantes ---
258
+ for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
259
+ if (!existingCols.includes(fieldName)) {
260
+ let colDef = getColumnDefinition(fieldName, field);
261
+
262
+ // Ajoute la colonne
263
+ const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN \`${fieldName}\` ${colDef}`;
264
+ await conn.promise().query(alterSQL);
265
+ logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
266
+ existingCols.push(fieldName);
267
+ }
268
+ }
269
+
270
+ // --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
271
+ if (dangerousSync) {
272
+ for (const col of existingCols) {
273
+ if (!Object.keys(model.schema.schemaDict).includes(col)) {
274
+ const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
275
+ await conn.promise().query(alterSQL);
276
+ logs(`Colonne ${col} supprimée de ${model.name}`);
277
+ }
278
+ }
279
+ }
118
280
  }
119
281
  // Vide la liste d'attente
120
282
  Model.pendingModels = [];
@@ -138,26 +300,8 @@ class Model {
138
300
 
139
301
  if (field.type && typeof field == "object") {
140
302
  // Si c'est un enum, ne pas vérifier sqlTypeMap
141
- let columnDefinition = "";
142
- if (Array.isArray(field.enum) && field.enum.length > 0) {
143
- const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
144
- columnDefinition = `${fieldName} ENUM(${enumValues})`;
145
- } else {
146
- if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
147
- columnDefinition = `${fieldName} ${sqlTypeMap[fieldType]}${sqlTypeMap[fieldType] == "VARCHAR" || sqlTypeMap[fieldType] == "INT" ? `(${field.length > 0 ? field.length : lengthDefault})` : ""}`;
148
- }
149
-
150
- if (field.required) columnDefinition += ' NOT NULL';
151
- if (field.default !== undefined && field.default != null) columnDefinition += ` DEFAULT "${field.default}"`;
152
- if (field.default === null) columnDefinition += ` DEFAULT NULL`;
153
- if (field.unique) columnDefinition += ' UNIQUE';
154
- if (field.auto_increment) columnDefinition += ' AUTO_INCREMENT';
155
- if (field.foreignKey) foreignKey.push(`FOREIGN KEY (${fieldName}) REFERENCES ${field.foreignKey}`);
156
- if (field.primary_key) columnDefinition += ' PRIMARY KEY';
157
- if (typeof field.customize === 'string' && field.customize.length != 0) columnDefinition += ` ${field.customize}`;
158
- return columnDefinition;
303
+ return getColumnDefinition(fieldName, field);
159
304
  }
160
-
161
305
  if (Array.isArray(field.enum) && field.enum.length > 0) {
162
306
  const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
163
307
  return `${fieldName} ENUM(${enumValues})`;
@@ -401,4 +545,4 @@ class Model {
401
545
  }
402
546
  }
403
547
 
404
- module.exports = {Model}
548
+ module.exports = { Model }
@@ -1,3 +1,5 @@
1
+ const generateCondition = require("../utils/generateCondition");
2
+
1
3
  /**
2
4
  * Represents an instance of a database model.
3
5
  * @class
package/client.d.ts DELETED
@@ -1,8 +0,0 @@
1
- declare module './client' {
2
- interface Client {
3
- [key: string]: any; // Dynamic methods added on the fly
4
- }
5
-
6
- const client: Client;
7
- export = client;
8
- }
package/client.js DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * @typedef {Object} Client
3
- * @property {Object.<string, any>} [dynamicMethods] - Dynamic methods added on the fly
4
- */
5
-
6
- /** @type {Client} */
7
- const client = {};
8
- module.exports = client;