@mlagie/sql-connector 1.4.9 → 2.0.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/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/workflows/publish.yml +47 -0
- package/README.md +260 -48
- package/docs/fr/README.md +204 -24
- package/eslint.config.mjs +12 -0
- package/index.d.ts +15 -45
- package/package.json +7 -3
- package/releases/1.5.0.md +127 -0
- package/releases/2.0.0.md +32 -0
- package/src/models/Model.js +110 -368
- package/src/models/ModelInstance.js +52 -10
- package/src/utils/buildQuery.js +72 -0
- package/src/utils/formatObject.js +6 -4
- package/src/utils/generateCondition.js +9 -6
- package/src/utils/security/safe.js +35 -0
package/src/models/Model.js
CHANGED
|
@@ -3,10 +3,10 @@ const { sqlTypeMap } = require("../utils/sqlTypeMap");
|
|
|
3
3
|
const { getConnexion } = require("../db/connexion");
|
|
4
4
|
const generateCondition = require("../utils/generateCondition");
|
|
5
5
|
const formatObject = require("../utils/formatObject");
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const glob = require('glob');
|
|
9
6
|
const { ModelInstance } = require("./ModelInstance");
|
|
7
|
+
const { buildSelect, buildQueryParts } = require("../utils/buildQuery");
|
|
8
|
+
const util = require("util");
|
|
9
|
+
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
10
10
|
|
|
11
11
|
function getFieldType(field) {
|
|
12
12
|
if (typeof field === "object") {
|
|
@@ -97,8 +97,8 @@ const reservedKeywords = ['ADD', 'ALL', 'ALTER', 'AND', 'AS', 'ASC', 'BETWEEN',
|
|
|
97
97
|
/**
|
|
98
98
|
* Checks if a table name is a reserved keyword.
|
|
99
99
|
*
|
|
100
|
-
* @param {string} tableName
|
|
101
|
-
* @returns {boolean} `true`
|
|
100
|
+
* @param {string} tableName The name of the table to be checked.
|
|
101
|
+
* @returns {boolean} `true` if the table name is a reserved keyword, otherwise `false`.
|
|
102
102
|
*
|
|
103
103
|
* @example
|
|
104
104
|
* const isReserved = ifReservedKeywords('SELECT');
|
|
@@ -127,8 +127,9 @@ function getColumnDefinition(fieldName, field) {
|
|
|
127
127
|
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
128
128
|
colDef = `ENUM(${enumValues})`;
|
|
129
129
|
} else {
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
const type = getSafe(sqlTypeMap, fieldType);
|
|
131
|
+
if (!type) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
132
|
+
colDef = `${type}${(type == "VARCHAR" || type == "INT") ? `(${field.length > 0 ? field.length : 255})` : ""}`;
|
|
132
133
|
}
|
|
133
134
|
if (field.required) colDef += ' NOT NULL';
|
|
134
135
|
const defaultDefinition = formatDefaultSql(field.default, fieldType);
|
|
@@ -156,15 +157,15 @@ class Model {
|
|
|
156
157
|
constructor(name, schema) {
|
|
157
158
|
this.name = name;
|
|
158
159
|
this.schema = schema;
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
Model.pendingModels.push(this);
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
/**
|
|
164
|
-
*
|
|
165
|
+
* Synchronizes all tables with their JS schemas (creation + adding missing columns).
|
|
165
166
|
* @returns {Promise<void>}
|
|
166
167
|
*/
|
|
167
|
-
static async syncAllTables(
|
|
168
|
+
static async syncAllTables() {
|
|
168
169
|
// Dépendances : {table: [tables dont elle dépend]}
|
|
169
170
|
const dependencies = {};
|
|
170
171
|
const modelMap = {};
|
|
@@ -179,284 +180,38 @@ class Model {
|
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
// Récupère toutes les tables existantes dans la base
|
|
183
183
|
const conn = getConnexion();
|
|
184
184
|
const [dbTablesRows] = await conn.promise().query("SHOW TABLES");
|
|
185
185
|
const dbTables = dbTablesRows.map(row => Object.values(row)[0]);
|
|
186
186
|
|
|
187
|
-
// Tri topologique
|
|
188
187
|
const sorted = [];
|
|
189
188
|
const visited = {};
|
|
190
189
|
function visit(table) {
|
|
191
|
-
if (visited
|
|
192
|
-
if (visited
|
|
193
|
-
visited
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
if (getSafe(visited, table) === true) return;
|
|
191
|
+
if (getSafe(visited, table) === 'temp') throw new Error('Cyclic foreign key dependency detected');
|
|
192
|
+
setSafe(visited, table, 'temp');
|
|
193
|
+
const deps = getSafe(dependencies, table)
|
|
194
|
+
for (const dep of deps) {
|
|
195
|
+
if (getSafe(modelMap, dep)) visit(dep);
|
|
196
196
|
}
|
|
197
|
-
visited
|
|
197
|
+
setSafe(visited, table, true);
|
|
198
198
|
sorted.push(table);
|
|
199
199
|
}
|
|
200
200
|
for (const table of Object.keys(dependencies)) {
|
|
201
|
-
if (!visited
|
|
201
|
+
if (!getSafe(visited, table)) visit(table);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
// --- Détruit les tables qui n'ont plus de schéma ---
|
|
205
|
-
for (const dbTable of dbTables) {
|
|
206
|
-
if (!modelMap[dbTable]) {
|
|
207
|
-
try {
|
|
208
|
-
const backupPath = `./backup_${dbTable}_${Date.now()}.sql`;
|
|
209
|
-
// Sauvegarde rapide en SQL (INSERTs)
|
|
210
|
-
const [rows] = await conn.promise().query(`SELECT * FROM \`${dbTable}\``);
|
|
211
|
-
if (rows.length > 0) {
|
|
212
|
-
const columns = Object.keys(rows[0]).map(col => `\`${col}\``).join(', ');
|
|
213
|
-
const values = rows.map(row =>
|
|
214
|
-
'(' + Object.values(row).map(val =>
|
|
215
|
-
val === null ? 'NULL' : conn.escape(val)
|
|
216
|
-
).join(', ') + ')'
|
|
217
|
-
).join(',\n');
|
|
218
|
-
const insertSQL = `INSERT INTO \`${dbTable}\` (${columns}) VALUES${values};\n`;
|
|
219
|
-
fs.writeFileSync(backupPath, insertSQL, 'utf-8');
|
|
220
|
-
logs(`Sauvegarde SQL de la table '${dbTable}' effectuée dans '${backupPath}'.`);
|
|
221
|
-
} else {
|
|
222
|
-
logs(`Table '${dbTable}' vide, fichier '${backupPath}' créé.`);
|
|
223
|
-
}
|
|
224
|
-
} catch (err) {
|
|
225
|
-
error(`Erreur lors de la sauvegarde SQL de la table '${dbTable}': ${err}`);
|
|
226
|
-
}
|
|
227
|
-
logs(`Table '${dbTable}' n'a plus de schéma associé, suppression...`);
|
|
228
|
-
await conn.promise().query(`DROP TABLE IF EXISTS \`${dbTable}\``);
|
|
229
|
-
logs(`Table '${dbTable}' supprimée.`);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Création/synchronisation des tables dans l'ordre
|
|
234
204
|
for (const table of sorted) {
|
|
235
|
-
const model = modelMap
|
|
205
|
+
const model = getSafe(modelMap, table);
|
|
236
206
|
|
|
237
|
-
// 1. Crée la table si elle n'existe pas (version avec promesses)
|
|
238
207
|
try {
|
|
239
208
|
await conn.promise().query(model.generateCreateTableStatement(model.schema.schemaDict));
|
|
240
|
-
await logs(`
|
|
209
|
+
await logs(`The table ${model.name} has been created or already exists`);
|
|
241
210
|
} catch (err) {
|
|
242
211
|
error(`Error creating table: ${err} with table name: ${model.name}`);
|
|
243
212
|
throw err;
|
|
244
213
|
}
|
|
245
|
-
|
|
246
|
-
if (dangerousSync) {
|
|
247
|
-
// --- Restauration automatique si backup SQL trouvé ---
|
|
248
|
-
const backupPattern = `./backup_${model.name}_*.sql`;
|
|
249
|
-
const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
|
|
250
|
-
if (backupFiles.length > 0) {
|
|
251
|
-
const latestBackup = backupFiles.sort().reverse()[0];
|
|
252
|
-
const readline = require('readline');
|
|
253
|
-
const rl = readline.createInterface({
|
|
254
|
-
input: process.stdin,
|
|
255
|
-
output: process.stdout
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Demande restauration
|
|
259
|
-
const answer = await new Promise((resolve) => {
|
|
260
|
-
rl.question(
|
|
261
|
-
`Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
|
|
262
|
-
(answer) => {
|
|
263
|
-
resolve(answer.trim().toLowerCase());
|
|
264
|
-
}
|
|
265
|
-
);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
if (answer === 'y') {
|
|
269
|
-
try {
|
|
270
|
-
const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
|
|
271
|
-
await conn.promise().query(sqlContent);
|
|
272
|
-
await logs(`Backup restauré pour la table '${model.name}'.`);
|
|
273
|
-
} catch (err) {
|
|
274
|
-
error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Toujours demander la suppression après restauration ou non
|
|
279
|
-
await new Promise((resolve) => {
|
|
280
|
-
rl.question(
|
|
281
|
-
`Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
|
|
282
|
-
(delAnswer) => {
|
|
283
|
-
if (delAnswer.trim().toLowerCase() === 'y') {
|
|
284
|
-
fs.unlinkSync(latestBackup);
|
|
285
|
-
logs(`Backup supprimé : ${latestBackup}`);
|
|
286
|
-
} else {
|
|
287
|
-
// Renomme le fichier pour ne plus proposer la restauration
|
|
288
|
-
fs.renameSync(latestBackup, latestBackup + '.ignored');
|
|
289
|
-
logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
|
|
290
|
-
}
|
|
291
|
-
rl.close();
|
|
292
|
-
resolve();
|
|
293
|
-
}
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// 2. Synchronise les colonnes (renommage, ajout, suppression)
|
|
299
|
-
const [columns] = await conn.promise().query(
|
|
300
|
-
`SHOW COLUMNS FROM \`${model.name}\``
|
|
301
|
-
);
|
|
302
|
-
let existingCols = columns.map(col => col.Field);
|
|
303
|
-
|
|
304
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
305
|
-
if (existingCols.includes(fieldName)) {
|
|
306
|
-
// 1. Récupère infos colonne
|
|
307
|
-
const currentCol = columns.find(col => col.Field === fieldName);
|
|
308
|
-
const [indexes] = await conn.promise().query(
|
|
309
|
-
`SHOW INDEX FROM \`${model.name}\` WHERE Column_name = ?`, [fieldName]
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
// 2. Vérifie les propriétés principales avec plus de précision
|
|
313
|
-
let needModify = false;
|
|
314
|
-
|
|
315
|
-
// --- Vérification du TYPE ---
|
|
316
|
-
const fieldType = getFieldType(field);
|
|
317
|
-
let expectedTypeDef;
|
|
318
|
-
|
|
319
|
-
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
320
|
-
// Pour les ENUM, on compare la définition complète
|
|
321
|
-
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
322
|
-
expectedTypeDef = `enum(${enumValues})`;
|
|
323
|
-
} else {
|
|
324
|
-
// Pour les autres types
|
|
325
|
-
if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
326
|
-
|
|
327
|
-
expectedTypeDef = sqlTypeMap[fieldType];
|
|
328
|
-
if (sqlTypeMap[fieldType] === "VARCHAR" || sqlTypeMap[fieldType] === "INT") {
|
|
329
|
-
const length = field.length > 0 ? field.length : 255;
|
|
330
|
-
expectedTypeDef += `(${length})`;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Normalisation pour la comparaison (minuscules, suppression des espaces)
|
|
335
|
-
const normalizeType = (typeStr) => {
|
|
336
|
-
return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
const currentTypeNormalized = normalizeType(currentCol.Type);
|
|
340
|
-
const expectedTypeNormalized = normalizeType(expectedTypeDef);
|
|
341
|
-
|
|
342
|
-
if (currentTypeNormalized !== expectedTypeNormalized) {
|
|
343
|
-
logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
|
|
344
|
-
needModify = true;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// --- Vérification NULL/NOT NULL ---
|
|
348
|
-
const shouldBeNotNull = field.required === true || field.primary_key === true;
|
|
349
|
-
const isNullableInDB = currentCol.Null === "YES";
|
|
350
|
-
|
|
351
|
-
if (shouldBeNotNull && isNullableInDB) {
|
|
352
|
-
logs(`Nullability mismatch for ${fieldName}: DB allows NULL, expected NOT NULL`);
|
|
353
|
-
needModify = true;
|
|
354
|
-
}
|
|
355
|
-
if (!shouldBeNotNull && !isNullableInDB) {
|
|
356
|
-
logs(`Nullability mismatch for ${fieldName}: DB is NOT NULL, expected NULL allowed`);
|
|
357
|
-
needModify = true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// --- Vérification DEFAULT VALUE ---
|
|
361
|
-
const expectedDefault = normalizeDefaultValue(field.default, fieldType);
|
|
362
|
-
const currentDefault = normalizeDbDefaultValue(currentCol.Default);
|
|
363
|
-
|
|
364
|
-
// Gestion spéciale pour les valeurs par défaut
|
|
365
|
-
if (expectedDefault !== null && currentDefault === null) {
|
|
366
|
-
logs(`Default value mismatch for ${fieldName}: DB has NULL, expected '${expectedDefault}'`);
|
|
367
|
-
needModify = true;
|
|
368
|
-
} else if (expectedDefault === null && currentDefault !== null) {
|
|
369
|
-
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected NULL`);
|
|
370
|
-
needModify = true;
|
|
371
|
-
} else if (expectedDefault !== null && currentDefault !== null) {
|
|
372
|
-
// Comparaison stringifiée pour éviter les problèmes de type
|
|
373
|
-
if (String(expectedDefault) !== String(currentDefault)) {
|
|
374
|
-
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected '${expectedDefault}'`);
|
|
375
|
-
needModify = true;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// --- Vérification UNIQUE constraint ---
|
|
380
|
-
const isUniqueInDB = indexes.some(idx => idx.Non_unique === 0 && idx.Key_name !== 'PRIMARY');
|
|
381
|
-
if (!!field.unique !== isUniqueInDB) {
|
|
382
|
-
logs(`Unique constraint mismatch for ${fieldName}: DB has unique=${isUniqueInDB}, expected=${!!field.unique}`);
|
|
383
|
-
needModify = true;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// --- Vérification PRIMARY KEY ---
|
|
387
|
-
const isPrimaryInDB = indexes.some(idx => idx.Key_name === 'PRIMARY');
|
|
388
|
-
if (!!field.primary_key !== isPrimaryInDB) {
|
|
389
|
-
logs(`Primary key mismatch for ${fieldName}: DB has PK=${isPrimaryInDB}, expected=${!!field.primary_key}`);
|
|
390
|
-
needModify = true;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// --- Vérification AUTO_INCREMENT ---
|
|
394
|
-
const isAutoIncrementInDB = currentCol.Extra.toLowerCase().includes('auto_increment');
|
|
395
|
-
if (!!field.auto_increment !== isAutoIncrementInDB) {
|
|
396
|
-
logs(`Auto increment mismatch for ${fieldName}: DB has AI=${isAutoIncrementInDB}, expected=${!!field.auto_increment}`);
|
|
397
|
-
needModify = true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// 3. Si différence, on modifie
|
|
401
|
-
if (needModify) {
|
|
402
|
-
try {
|
|
403
|
-
const newColDef = getColumnDefinition(fieldName, field);
|
|
404
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` MODIFY COLUMN ${newColDef}`;
|
|
405
|
-
logs(`Modifying column ${fieldName}: ${alterSQL}`);
|
|
406
|
-
await conn.promise().query(alterSQL);
|
|
407
|
-
logs(`Column ${fieldName} modified successfully`);
|
|
408
|
-
} catch (err) {
|
|
409
|
-
error(`Error modifying column ${fieldName}: ${err}`);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
// --- Warn si oldName est présent dans le schéma ---
|
|
414
|
-
if (field.oldName) {
|
|
415
|
-
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.`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// --- Étape 1 : Renommage des colonnes ---
|
|
420
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
421
|
-
if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
|
|
422
|
-
// Génère la définition SQL de la nouvelle colonne
|
|
423
|
-
let colDef = getColumnDefinition(fieldName, field);
|
|
424
|
-
|
|
425
|
-
// Renomme la colonne
|
|
426
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` ${colDef}`;
|
|
427
|
-
await conn.promise().query(alterSQL);
|
|
428
|
-
logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
|
|
429
|
-
// Mets à jour existingCols pour la suite
|
|
430
|
-
existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// --- Étape 2 : Ajout des colonnes manquantes ---
|
|
435
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
436
|
-
if (!existingCols.includes(fieldName)) {
|
|
437
|
-
let colDef = getColumnDefinition(fieldName, field);
|
|
438
|
-
|
|
439
|
-
// Ajoute la colonne
|
|
440
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN ${colDef}`;
|
|
441
|
-
await conn.promise().query(alterSQL);
|
|
442
|
-
logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
|
|
443
|
-
existingCols.push(fieldName);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
|
|
448
|
-
if (dangerousSync) {
|
|
449
|
-
for (const col of existingCols) {
|
|
450
|
-
if (!Object.keys(model.schema.schemaDict).includes(col)) {
|
|
451
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
|
|
452
|
-
await conn.promise().query(alterSQL);
|
|
453
|
-
logs(`Colonne ${col} supprimée de ${model.name}`);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
214
|
}
|
|
459
|
-
// Vide la liste d'attente
|
|
460
215
|
Model.pendingModels = [];
|
|
461
216
|
}
|
|
462
217
|
|
|
@@ -469,7 +224,7 @@ class Model {
|
|
|
469
224
|
generateCreateTableStatement(schema) {
|
|
470
225
|
let foreignKey = [];
|
|
471
226
|
const columns = Object.keys(schema).map(fieldName => {
|
|
472
|
-
|
|
227
|
+
setSafe(field, schema, fieldName);
|
|
473
228
|
let lengthDefault = 255;
|
|
474
229
|
|
|
475
230
|
if (!field.type && typeof field == "object" && !(Array.isArray(field.enum) && field.enum.length > 0)) throw new Error(`Field ${fieldName} has no type defined.`);
|
|
@@ -477,7 +232,6 @@ class Model {
|
|
|
477
232
|
const fieldType = getFieldType(field);
|
|
478
233
|
|
|
479
234
|
if (field.type && typeof field == "object") {
|
|
480
|
-
// Si c'est un enum, ne pas vérifier sqlTypeMap
|
|
481
235
|
return getColumnDefinition(fieldName, field);
|
|
482
236
|
}
|
|
483
237
|
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
@@ -485,9 +239,11 @@ class Model {
|
|
|
485
239
|
return `${fieldName} ENUM(${enumValues})`;
|
|
486
240
|
}
|
|
487
241
|
|
|
488
|
-
|
|
242
|
+
const type = getSafe(sqlTypeMap, fieldType);
|
|
489
243
|
|
|
490
|
-
|
|
244
|
+
if (!type) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
|
|
245
|
+
|
|
246
|
+
return `${fieldName} ${type == "VARCHAR" ? `${type}(${lengthDefault})` : type}`;
|
|
491
247
|
});
|
|
492
248
|
if (ifReservedKeywords(this.name)) {
|
|
493
249
|
error("Error: Invalid table name. Please choose a different name that is not a reserved keyword in SQL_request");
|
|
@@ -496,6 +252,18 @@ class Model {
|
|
|
496
252
|
return `CREATE TABLE IF NOT EXISTS ${this.name} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
|
|
497
253
|
}
|
|
498
254
|
|
|
255
|
+
getRecordData() {
|
|
256
|
+
return Array.isArray(this.data) ? this.data[0] ?? this.data : this.data;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
toJSON() {
|
|
260
|
+
return this.getRecordData()?.data;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
[util.inspect.custom]() {
|
|
264
|
+
return this.getRecordData();
|
|
265
|
+
}
|
|
266
|
+
|
|
499
267
|
/**
|
|
500
268
|
* Saves data to the database table.
|
|
501
269
|
* @param {Object} data The data to insert into the table.
|
|
@@ -508,7 +276,7 @@ class Model {
|
|
|
508
276
|
|
|
509
277
|
try {
|
|
510
278
|
const result = await getConnexion().promise().query(sql_request);
|
|
511
|
-
return result;
|
|
279
|
+
return result[0];
|
|
512
280
|
} catch (err) {
|
|
513
281
|
error(`Error inserting data into ${this.name}: ${err}`);
|
|
514
282
|
throw err;
|
|
@@ -516,109 +284,85 @@ class Model {
|
|
|
516
284
|
}
|
|
517
285
|
|
|
518
286
|
/**
|
|
519
|
-
*
|
|
520
|
-
* @
|
|
521
|
-
* @
|
|
522
|
-
* @
|
|
523
|
-
* @
|
|
524
|
-
* @param {number} [options.limit] - Limite de résultats.
|
|
525
|
-
* @returns {Promise<Array<ModelInstance>>}
|
|
287
|
+
* @typedef {Object} SelectAggregation
|
|
288
|
+
* @property {string} [sum] - The name of the column to sum (e.g., "total_runs").
|
|
289
|
+
* @property {string} [count] - The name of the column to count.
|
|
290
|
+
* @property {string[]} [dateFormat] - Array with [column, format] (e.g., ["date_day", "%Y-%m-%d"]).
|
|
291
|
+
* @property {string} as - The output alias for the SQL field (e.g., "total_runs" or "period").
|
|
526
292
|
*/
|
|
527
|
-
async findAll(options = {}) {
|
|
528
|
-
const {
|
|
529
|
-
attributes = ['*'],
|
|
530
|
-
where = undefined,
|
|
531
|
-
order = undefined,
|
|
532
|
-
limit = undefined
|
|
533
|
-
} = options;
|
|
534
|
-
|
|
535
|
-
let sql_request = `SELECT ${attributes.join(', ')} FROM ${this.name}`;
|
|
536
|
-
if (where) {
|
|
537
|
-
sql_request += ` WHERE ${generateCondition(formatObject(where))}`;
|
|
538
|
-
}
|
|
539
|
-
if (order && Array.isArray(order) && order.length > 0) {
|
|
540
|
-
const orderStr = order.map(([col, dir]) => `${col} ${dir}`).join(', ');
|
|
541
|
-
sql_request += ` ORDER BY ${orderStr}`;
|
|
542
|
-
}
|
|
543
|
-
if (limit) {
|
|
544
|
-
sql_request += ` LIMIT ${limit}`;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return new Promise((resolve, reject) => {
|
|
548
|
-
getConnexion().promise().query(sql_request).then(([rows]) => {
|
|
549
|
-
const instances = rows.map(row => new ModelInstance(this.name, row, this.schema));
|
|
550
|
-
resolve(instances);
|
|
551
|
-
}).catch((err) => {
|
|
552
|
-
error(`Error executing findAll: ${err}`);
|
|
553
|
-
resolve([]);
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
293
|
|
|
558
294
|
/**
|
|
559
|
-
*
|
|
560
|
-
* @param {Object}
|
|
561
|
-
* @param {string
|
|
562
|
-
* @
|
|
295
|
+
* Retrieves multiple entries from the table.
|
|
296
|
+
* @param {Object} [options] - Query options (attributes, where, order, limit).
|
|
297
|
+
* @param {Array<string|SelectAggregation>} [options.select] - Fields to return.
|
|
298
|
+
* @param {Object} [options.where] - Filters (key/value).
|
|
299
|
+
* @param {Array} [options.order] - Example: [['points', 'DESC']]
|
|
300
|
+
* @param {number} [options.limit] - Result limit.
|
|
301
|
+
* @param {Object} [options.join] - Join options.
|
|
302
|
+
* @param {String} [options.join.table] - Table to join.
|
|
303
|
+
* @param {String} [options.join.on] - Join condition.
|
|
304
|
+
* @param {String} [options.join.alias] - Alias for the joined table.
|
|
305
|
+
* @returns {Promise<Array<ModelInstance>>}
|
|
563
306
|
*/
|
|
564
|
-
async
|
|
565
|
-
|
|
307
|
+
async find(options = {}) {
|
|
308
|
+
let { select, join } = options;
|
|
309
|
+
|
|
310
|
+
if (join && join.table && select) {
|
|
311
|
+
select = select.map(item => {
|
|
312
|
+
if (typeof item === 'string') {
|
|
313
|
+
if (!item.includes('.')) {
|
|
314
|
+
if (item.startsWith('name')) {
|
|
315
|
+
return `${join.table}.${item}`;
|
|
316
|
+
}
|
|
317
|
+
return `${this.name}.${item}`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else if (typeof item === 'object') {
|
|
321
|
+
const key = Object.keys(item)[0];
|
|
566
322
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
323
|
+
if (Array.isArray(getSafe(item, key))) {
|
|
324
|
+
setSafe(item, key, getSafe(item, key).map((param, index) => {
|
|
325
|
+
if (index === 0 && typeof param === 'string' && !param.includes('.')) {
|
|
326
|
+
return `${this.name}.${param}`;
|
|
327
|
+
}
|
|
328
|
+
return param;
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
else if (typeof getSafe(item, key) === 'string' && !getSafe(item, key).includes('.')) {
|
|
332
|
+
setSafe(item, key, `${this.name}.${getSafe(item, key)}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return item;
|
|
574
336
|
});
|
|
575
|
-
}
|
|
576
|
-
|
|
337
|
+
}
|
|
338
|
+
let joinClause = "";
|
|
339
|
+
if (join && join.table && join.on) {
|
|
340
|
+
joinClause = ` INNER JOIN ${join.table} ON ${join.on}`;
|
|
341
|
+
}
|
|
577
342
|
|
|
578
|
-
|
|
579
|
-
* Finds a record in the database based on the provided filter.
|
|
580
|
-
*
|
|
581
|
-
* @async
|
|
582
|
-
* @param {Object} filter - The filter criteria for the query. Should be an object where keys are column names and values are the values to filter by.
|
|
583
|
-
* @param {Array<string>} [fields=["*"]] - The fields to select in the query. Defaults to selecting all fields.
|
|
584
|
-
* @returns {Promise<ModelInstance|number>} - A promise that resolves to a `ModelInstance` if a record is found, or `0` if no records match the filter.
|
|
585
|
-
*
|
|
586
|
-
* @example
|
|
587
|
-
* // Example usage:
|
|
588
|
-
* const filter = { id: 1 };
|
|
589
|
-
* const fields = ["id", "name"];
|
|
590
|
-
* MyTable.find(filter, fields).then((result) => {
|
|
591
|
-
* if (result === 0) {
|
|
592
|
-
* console.log("No records found.");
|
|
593
|
-
* } else {
|
|
594
|
-
* console.log("Record found:", result);
|
|
595
|
-
* }
|
|
596
|
-
* }).catch((err) => {
|
|
597
|
-
* console.error("Error:", err);
|
|
598
|
-
* });
|
|
599
|
-
*/
|
|
600
|
-
async find(filter, fields = ["*"]) {
|
|
601
|
-
const sql_request = `SELECT ${fields.join(", ")} FROM ${this.name} WHERE ${generateCondition(formatObject(filter))}`;
|
|
343
|
+
const query = `SELECT ${buildSelect(select)} FROM ${this.name}${joinClause} ${buildQueryParts(options)}`;
|
|
602
344
|
|
|
603
|
-
return new Promise((resolve, reject) => {
|
|
604
|
-
getConnexion().promise().query(
|
|
605
|
-
|
|
345
|
+
return new Promise(async (resolve, reject) => {
|
|
346
|
+
await getConnexion().promise().query(query).then((result) => {
|
|
347
|
+
const rows = result && Array.isArray(result) ? result[0] : result;
|
|
348
|
+
if (!rows || rows.length === 0) return resolve([]);
|
|
606
349
|
|
|
607
|
-
|
|
350
|
+
const instances = rows.map(row => new ModelInstance(this.name, row, this.schema));
|
|
351
|
+
resolve(instances);
|
|
608
352
|
}).catch((err) => {
|
|
609
|
-
error(`Error executing
|
|
610
|
-
|
|
353
|
+
error(`Error executing auto-prefixed find: ${err}`);
|
|
354
|
+
reject(err);
|
|
611
355
|
});
|
|
612
356
|
});
|
|
613
357
|
}
|
|
614
358
|
|
|
615
359
|
/**
|
|
616
|
-
*
|
|
360
|
+
* Counts the number of records matching the given filter.
|
|
617
361
|
* @param {Object} filter The filter criteria for the query. Should be an object where keys are column names and values are the values to filter by.
|
|
618
362
|
* @returns {Promise<ModelInstance|number>} - A promise that resolves to a `ModelInstance` if a record is found, or `0` if no records match the filter.
|
|
619
363
|
*/
|
|
620
364
|
async count(filter) {
|
|
621
|
-
return this.customRequest(`SELECT COUNT(*) as count FROM ${this.name} ${filter != undefined ? `WHERE ${generateCondition(formatObject(filter))}` : ""}
|
|
365
|
+
return this.customRequest(`SELECT COUNT(*) as count FROM ${this.name} ${filter != undefined ? `WHERE ${generateCondition(formatObject(filter))}` : ""}`, "count");
|
|
622
366
|
}
|
|
623
367
|
|
|
624
368
|
/**
|
|
@@ -627,28 +371,26 @@ class Model {
|
|
|
627
371
|
* @returns {Promise<void>} A promise that resolves when the query is executed.
|
|
628
372
|
* @throws {Error} Throws an error if query execution fails.
|
|
629
373
|
*/
|
|
630
|
-
async customRequest(custom) {
|
|
374
|
+
async customRequest(custom, custom_err_name = "") {
|
|
631
375
|
return new Promise(async (resolve, reject) => {
|
|
632
376
|
await getConnexion().promise().query(custom).then((rows) => {
|
|
633
377
|
if (rows.length == 0) return resolve(0);
|
|
634
378
|
|
|
635
|
-
resolve(new ModelInstance(this.name, rows
|
|
379
|
+
resolve(new ModelInstance(this.name, rows, this.schema));
|
|
636
380
|
}).catch((err) => {
|
|
637
|
-
error(`Error executing query: ${err}`);
|
|
381
|
+
error(`Error executing query ${custom_err_name}: ${err}`);
|
|
638
382
|
return;
|
|
639
383
|
});
|
|
640
384
|
})
|
|
641
385
|
}
|
|
642
386
|
|
|
643
387
|
/**
|
|
644
|
-
*
|
|
388
|
+
* Deletes an entry from the SQL table that matches the provided filter.
|
|
645
389
|
*
|
|
646
|
-
* @
|
|
647
|
-
* @
|
|
648
|
-
*
|
|
649
|
-
* @
|
|
650
|
-
* ou à une instance de ModelInstance représentant la ligne supprimée.
|
|
651
|
-
* @throws {Error} Lance une erreur si la requête SQL échoue.
|
|
390
|
+
* @param {Object} filter An object representing the filter conditions for deletion.
|
|
391
|
+
* @returns {Promise<number>} A promise that resolves to 0 if no rows were deleted,
|
|
392
|
+
* or to a ModelInstance representing the deleted row.
|
|
393
|
+
* @throws {Error} Throws an error if the SQL query fails.
|
|
652
394
|
*/
|
|
653
395
|
async delete(filter) {
|
|
654
396
|
const sql_request = `DELETE FROM ${this.name} WHERE ${generateCondition(formatObject(filter))}`;
|
|
@@ -658,7 +400,7 @@ class Model {
|
|
|
658
400
|
|
|
659
401
|
return resolve(1);
|
|
660
402
|
}).catch((err) => {
|
|
661
|
-
error(`Error executing query: ${err}`);
|
|
403
|
+
error(`Error executing query delete: ${err}`);
|
|
662
404
|
return 0;
|
|
663
405
|
});
|
|
664
406
|
});
|
|
@@ -681,7 +423,7 @@ class Model {
|
|
|
681
423
|
getConnexion().promise().query(sql_request).then((rows) => {
|
|
682
424
|
console.log(rows);
|
|
683
425
|
}).catch((err) => {
|
|
684
|
-
error(`Error executing query: ${err}`);
|
|
426
|
+
error(`Error executing query drop: ${err}`);
|
|
685
427
|
return;
|
|
686
428
|
});
|
|
687
429
|
})
|
|
@@ -715,7 +457,7 @@ class Model {
|
|
|
715
457
|
if (rows[0][0]['COUNT(*)'] == 0) return resolve(uuid);
|
|
716
458
|
resolve(null);
|
|
717
459
|
}).catch((err) => {
|
|
718
|
-
error(`Error executing query: ${err}`);
|
|
460
|
+
error(`Error executing query gen_uuid: ${err}`);
|
|
719
461
|
return null;
|
|
720
462
|
})
|
|
721
463
|
})
|