@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 +57 -0
- package/index.d.ts +1 -1
- package/index.js +2 -1
- package/package.json +7 -3
- package/src/models/Model.js +176 -33
- package/client.d.ts +0 -8
- package/client.js +0 -8
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"/>     
|
|
3
|
+
 
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": [
|
|
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
|
-
"
|
|
27
|
+
"glob": "^11.0.3",
|
|
28
|
+
"mysql2": "3.14.5"
|
|
25
29
|
}
|
|
26
30
|
}
|
package/src/models/Model.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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 [
|
|
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
|
-
//
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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