@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.
@@ -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 Le nom de la table à vérifier.
101
- * @returns {boolean} `true` si le nom de la table est un mot-clé réservé, sinon `false`.
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
- if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
131
- colDef = `${sqlTypeMap[fieldType]}${(sqlTypeMap[fieldType] == "VARCHAR" || sqlTypeMap[fieldType] == "INT") ? `(${field.length > 0 ? field.length : 255})` : ""}`;
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
- // Ajoute le modèle à la liste d'attente pour la création différée
160
+
160
161
  Model.pendingModels.push(this);
161
162
  }
162
163
 
163
164
  /**
164
- * Synchronise toutes les tables avec leur schéma JS (création + ajout des colonnes manquantes).
165
+ * Synchronizes all tables with their JS schemas (creation + adding missing columns).
165
166
  * @returns {Promise<void>}
166
167
  */
167
- static async syncAllTables({ dangerousSync = false } = {}) {
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[table] === true) return;
192
- if (visited[table] === 'temp') throw new Error('Cyclic foreign key dependency detected');
193
- visited[table] = 'temp';
194
- for (const dep of dependencies[table]) {
195
- if (modelMap[dep]) visit(dep);
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[table] = true;
197
+ setSafe(visited, table, true);
198
198
  sorted.push(table);
199
199
  }
200
200
  for (const table of Object.keys(dependencies)) {
201
- if (!visited[table]) visit(table);
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[table];
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(`La table ${model.name} a été créée ou existe déjà`);
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
- const field = schema[fieldName];
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
- if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
242
+ const type = getSafe(sqlTypeMap, fieldType);
489
243
 
490
- return `${fieldName} ${sqlTypeMap[fieldType] == "VARCHAR" ? `${sqlTypeMap[fieldType]}(${lengthDefault})` : sqlTypeMap[fieldType]}`;
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
- * Récupère plusieurs entrées de la table.
520
- * @param {Object} [options] - Options de requête (attributs, where, order, limit).
521
- * @param {string[]} [options.attributes] - Champs à retourner.
522
- * @param {Object} [options.where] - Filtres (clé/valeur).
523
- * @param {Array} [options.order] - Ex: [['points', 'DESC']]
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
- * Finds a unique entry in the database table based on the filter provided.
560
- * @param {Object} filter An object containing the key-value pairs to use to generate the search condition.
561
- * @param {string[]} [fields=["*"]] An array of field names to return in the result.
562
- * @returns {Promise<ModelInstance|number>} A promise that resolves to a ModelInstance if an entry is found, otherwise 0.
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 findOne(filter, fields = ["*"]) {
565
- const sql_request = `SELECT ${fields.join(", ")} FROM ${this.name} WHERE ${generateCondition(formatObject(filter))}`;
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
- return new Promise((resolve, reject) => {
568
- getConnexion().promise().query(sql_request).then(([rows]) => {
569
- if (!rows || rows.length === 0) return resolve(0);
570
- resolve(new ModelInstance(this.name, rows[0], this.schema));
571
- }).catch((err) => {
572
- error(`Error executing query: ${err}`);
573
- return resolve(0);
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(sql_request).then((rows) => {
605
- if (rows.length == 0) return resolve(0);
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
- resolve(new ModelInstance(this.name, rows[0], this.schema));
350
+ const instances = rows.map(row => new ModelInstance(this.name, row, this.schema));
351
+ resolve(instances);
608
352
  }).catch((err) => {
609
- error(`Error executing query: ${err}`);
610
- return;
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[0], this.schema));
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
- * Supprime une entrée de la table SQL correspondant au filtre fourni.
388
+ * Deletes an entry from the SQL table that matches the provided filter.
645
389
  *
646
- * @async
647
- * @function delete
648
- * @param {Object} filter - Un objet représentant les conditions de filtre pour la suppression.
649
- * @returns {Promise<number>} Une promesse qui se résout à 0 si aucune ligne n'a été supprimée,
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
  })