@mlagie/sql-connector 1.1.4-beta → 1.2.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
@@ -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.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": {
@@ -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
  }
@@ -45,6 +45,27 @@ function ifReservedKeywords(tableName) {
45
45
  return false;
46
46
  }
47
47
 
48
+ function getColumnDefinition(fieldName, field) {
49
+ const fieldType = getFieldType(field);
50
+ let colDef = "";
51
+
52
+ if (Array.isArray(field.enum) && field.enum.length > 0) {
53
+ const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
54
+ colDef = `ENUM(${enumValues})`;
55
+ } else {
56
+ if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
57
+ colDef = `${sqlTypeMap[fieldType]}${(sqlTypeMap[fieldType] == "VARCHAR" || sqlTypeMap[fieldType] == "INT") ? `(${field.length > 0 ? field.length : 255})` : ""}`;
58
+ }
59
+ if (field.required) colDef += ' NOT NULL';
60
+ if (field.default !== undefined && field.default != null) colDef += ` DEFAULT "${field.default}"`;
61
+ if (field.default === null) colDef += ` DEFAULT NULL`;
62
+ if (field.unique) colDef += ' UNIQUE';
63
+ if (field.auto_increment) colDef += ' AUTO_INCREMENT';
64
+ if (field.primary_key) colDef += ' PRIMARY KEY';
65
+ if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
66
+ return colDef;
67
+ }
68
+
48
69
  /**
49
70
  * Represents a database model.
50
71
  * @class
@@ -66,25 +87,33 @@ class Model {
66
87
  }
67
88
 
68
89
  /**
69
- * Crée toutes les tables dans l'ordre correct en fonction des foreign keys.
90
+ * Synchronise toutes les tables avec leur schéma JS (création + ajout des colonnes manquantes).
70
91
  * @returns {Promise<void>}
71
92
  */
72
- static async createAllTables() {
93
+ static async syncAllTables({ dangerousSync = false } = {}) {
94
+ const fs = require('fs');
95
+ const path = require('path');
96
+ const glob = require('glob');
97
+
73
98
  // Dépendances : {table: [tables dont elle dépend]}
74
99
  const dependencies = {};
75
100
  const modelMap = {};
76
101
  for (const model of Model.pendingModels) {
77
102
  modelMap[model.name] = model;
78
103
  dependencies[model.name] = [];
79
- for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
104
+ for (const [_, field] of Object.entries(model.schema.schemaDict)) {
80
105
  if (field && field.foreignKey) {
81
- // field.foreignKey peut être "autreTable(colonne)"
82
106
  const refTable = field.foreignKey.split('(')[0].trim();
83
107
  dependencies[model.name].push(refTable);
84
108
  }
85
109
  }
86
110
  }
87
111
 
112
+ // Récupère toutes les tables existantes dans la base
113
+ const conn = getConnexion();
114
+ const [dbTablesRows] = await conn.promise().query("SHOW TABLES");
115
+ const dbTables = dbTablesRows.map(row => Object.values(row)[0]);
116
+
88
117
  // Tri topologique
89
118
  const sorted = [];
90
119
  const visited = {};
@@ -102,19 +131,151 @@ class Model {
102
131
  if (!visited[table]) visit(table);
103
132
  }
104
133
 
105
- // Création des tables dans l'ordre
134
+ // --- Détruit les tables qui n'ont plus de schéma ---
135
+ for (const dbTable of dbTables) {
136
+ if (!modelMap[dbTable]) {
137
+ try {
138
+ const backupPath = `./backup_${dbTable}_${Date.now()}.sql`;
139
+ // Sauvegarde rapide en SQL (INSERTs)
140
+ const [rows] = await conn.promise().query(`SELECT * FROM \`${dbTable}\``);
141
+ if (rows.length > 0) {
142
+ const columns = Object.keys(rows[0]).map(col => `\`${col}\``).join(', ');
143
+ const values = rows.map(row =>
144
+ '(' + Object.values(row).map(val =>
145
+ val === null ? 'NULL' : conn.escape(val)
146
+ ).join(', ') + ')'
147
+ ).join(',\n');
148
+ const insertSQL = `INSERT INTO \`${dbTable}\` (${columns}) VALUES${values};\n`;
149
+ fs.writeFileSync(backupPath, insertSQL, 'utf-8');
150
+ logs(`Sauvegarde SQL de la table '${dbTable}' effectuée dans '${backupPath}'.`);
151
+ } else {
152
+ fs.writeFileSync(backupPath, '', 'utf-8');
153
+ logs(`Table '${dbTable}' vide, fichier '${backupPath}' créé.`);
154
+ }
155
+ } catch (err) {
156
+ error(`Erreur lors de la sauvegarde SQL de la table '${dbTable}': ${err}`);
157
+ }
158
+ logs(`Table '${dbTable}' n'a plus de schéma associé, suppression...`);
159
+ await conn.promise().query(`DROP TABLE IF EXISTS \`${dbTable}\``);
160
+ logs(`Table '${dbTable}' supprimée.`);
161
+ }
162
+ }
163
+
164
+ // Création/synchronisation des tables dans l'ordre
106
165
  for (const table of sorted) {
107
166
  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);
167
+
168
+ // 1. Crée la table si elle n'existe pas (version avec promesses)
169
+ try {
170
+ await conn.promise().query(model.generateCreateTableStatement(model.schema.schemaDict));
171
+ await logs(`La table ${model.name} a été créée ou existe déjà`);
172
+ } catch (err) {
173
+ error(`Error creating table: ${err} with table name: ${model.name}`);
174
+ throw err;
175
+ }
176
+
177
+ // --- Restauration automatique si backup SQL trouvé ---
178
+ const backupPattern = `./backup_${model.name}_*.sql`;
179
+ const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
180
+ if (backupFiles.length > 0) {
181
+ const latestBackup = backupFiles.sort().reverse()[0];
182
+ const readline = require('readline');
183
+ const rl = readline.createInterface({
184
+ input: process.stdin,
185
+ output: process.stdout
186
+ });
187
+
188
+ // Demande restauration
189
+ const answer = await new Promise((resolve) => {
190
+ rl.question(
191
+ `Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
192
+ (answer) => {
193
+ resolve(answer.trim().toLowerCase());
194
+ }
195
+ );
196
+ });
197
+
198
+ if (answer === 'y') {
199
+ try {
200
+ const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
201
+ await conn.promise().query(sqlContent);
202
+ await logs(`Backup restauré pour la table '${model.name}'.`);
203
+ } catch (err) {
204
+ error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
113
205
  }
114
- logs(`La table ${model.name} a été créé`);
115
- resolve();
206
+ }
207
+
208
+ // Toujours demander la suppression après restauration ou non
209
+ await new Promise((resolve) => {
210
+ rl.question(
211
+ `Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
212
+ (delAnswer) => {
213
+ if (delAnswer.trim().toLowerCase() === 'y') {
214
+ fs.unlinkSync(latestBackup);
215
+ logs(`Backup supprimé : ${latestBackup}`);
216
+ } else {
217
+ // Renomme le fichier pour ne plus proposer la restauration
218
+ fs.renameSync(latestBackup, latestBackup + '.ignored');
219
+ logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
220
+ }
221
+ rl.close();
222
+ resolve();
223
+ }
224
+ );
116
225
  });
117
- });
226
+ }
227
+
228
+ // 2. Synchronise les colonnes (renommage, ajout, suppression)
229
+ const [columns] = await conn.promise().query(
230
+ `SHOW COLUMNS FROM \`${model.name}\``
231
+ );
232
+ let existingCols = columns.map(col => col.Field);
233
+
234
+ // --- Warn si oldName est présent dans le schéma ---
235
+ for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
236
+ if (field.oldName) {
237
+ 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.`);
238
+ }
239
+ }
240
+
241
+ // --- Étape 1 : Renommage des colonnes ---
242
+ for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
243
+ if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
244
+ // Génère la définition SQL de la nouvelle colonne
245
+ let colDef = getColumnDefinition(fieldName, field);
246
+
247
+ // Renomme la colonne
248
+ const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` \`${fieldName}\` ${colDef}`;
249
+ await conn.promise().query(alterSQL);
250
+ logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
251
+ // Mets à jour existingCols pour la suite
252
+ existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
253
+ }
254
+ }
255
+
256
+ // --- Étape 2 : Ajout des colonnes manquantes ---
257
+ for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
258
+ if (!existingCols.includes(fieldName)) {
259
+ let colDef = getColumnDefinition(fieldName, field);
260
+
261
+ // Ajoute la colonne
262
+ const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN \`${fieldName}\` ${colDef}`;
263
+ await conn.promise().query(alterSQL);
264
+ logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
265
+ existingCols.push(fieldName);
266
+ }
267
+ }
268
+
269
+ // --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
270
+ if (dangerousSync) {
271
+ for (const col of existingCols) {
272
+ if (!Object.keys(model.schema.schemaDict).includes(col)) {
273
+ const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
274
+ await conn.promise().query(alterSQL);
275
+ logs(`Colonne ${col} supprimée de ${model.name}`);
276
+ }
277
+ }
278
+ }
118
279
  }
119
280
  // Vide la liste d'attente
120
281
  Model.pendingModels = [];
@@ -138,26 +299,8 @@ class Model {
138
299
 
139
300
  if (field.type && typeof field == "object") {
140
301
  // 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;
302
+ return getColumnDefinition(fieldName, field);
159
303
  }
160
-
161
304
  if (Array.isArray(field.enum) && field.enum.length > 0) {
162
305
  const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
163
306
  return `${fieldName} ENUM(${enumValues})`;
@@ -401,4 +544,4 @@ class Model {
401
544
  }
402
545
  }
403
546
 
404
- module.exports = {Model}
547
+ module.exports = { Model }
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;