@mlagie/sql-connector 2.0.1 → 2.0.3
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/eslint.config.mjs +26 -1
- package/package.json +4 -2
- package/releases/2.0.2.md +20 -0
- package/releases/2.0.3.md +20 -0
- package/src/models/Model.js +42 -82
- package/src/models/ModelInstance.js +27 -36
- package/src/utils/buildQuery.js +16 -11
- package/src/utils/formatObject.js +1 -1
- package/src/utils/generateCondition.js +14 -11
- package/src/utils/sql.js +44 -0
package/eslint.config.mjs
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
1
2
|
import security from "eslint-plugin-security";
|
|
2
3
|
|
|
3
4
|
export default [
|
|
4
5
|
{
|
|
6
|
+
ignores: ["eslint.config.mjs"]
|
|
7
|
+
},
|
|
8
|
+
js.configs.recommended,
|
|
9
|
+
{
|
|
10
|
+
files: ["**/*.js"],
|
|
11
|
+
languageOptions: {
|
|
12
|
+
sourceType: "commonjs",
|
|
13
|
+
globals: {
|
|
14
|
+
console: "readonly",
|
|
15
|
+
exports: "readonly",
|
|
16
|
+
module: "readonly",
|
|
17
|
+
process: "readonly",
|
|
18
|
+
require: "readonly",
|
|
19
|
+
__dirname: "readonly",
|
|
20
|
+
__filename: "readonly"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
5
23
|
plugins: {
|
|
6
24
|
security
|
|
7
25
|
},
|
|
8
26
|
rules: {
|
|
9
|
-
...security.configs.recommended.rules
|
|
27
|
+
...security.configs.recommended.rules,
|
|
28
|
+
"no-unused-vars": [
|
|
29
|
+
"error",
|
|
30
|
+
{
|
|
31
|
+
argsIgnorePattern: "^(?:_|resolve|reject)$",
|
|
32
|
+
varsIgnorePattern: "^_$"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
10
35
|
}
|
|
11
36
|
}
|
|
12
37
|
];
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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": {
|
|
7
|
-
"security": "npx eslint . --max-warnings 0"
|
|
7
|
+
"security": "npx eslint . --max-warnings 0",
|
|
8
|
+
"security:socket": "npx socket scan create --auto-manifest --report ."
|
|
8
9
|
},
|
|
9
10
|
"repository": {
|
|
10
11
|
"type": "git",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"mysql2": "3.22.5"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
43
|
+
"@eslint/js": "^10.0.1",
|
|
42
44
|
"eslint": "^10.6.0",
|
|
43
45
|
"eslint-plugin-security": "^4.0.1"
|
|
44
46
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Release v2.0.2 — Performance Optimization & Code Cleanup
|
|
2
|
+
|
|
3
|
+
This patch release focuses on optimizing the internal runtime footprint and improving overall execution performance by removing legacy, unused code paths and dependencies.
|
|
4
|
+
|
|
5
|
+
## What Was Improved
|
|
6
|
+
|
|
7
|
+
* **Dead Code Elimination:** Removed unused functions, legacy variables, and obsolete logic that were lingering from previous architecture overhauls (such as the old `serveur` logger reference).
|
|
8
|
+
* **Performance Boost:** Cleaning up these elements reduces CPU overhead and memory usage during runtime model instantiation and schema synchronization.
|
|
9
|
+
* **Code Maintenance:** Streamlined internal loops and conditional structures to ensure the ORM remains fast, lightweight, and maintainable.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Component Impact
|
|
14
|
+
|
|
15
|
+
| Impacted Area | Description | Status |
|
|
16
|
+
| :--- | :--- | :--- |
|
|
17
|
+
| **ModelInstance & Models** | Removal of unused references and obsolete properties | **Optimized** |
|
|
18
|
+
| **Runtime Footprint** | Decreased execution overhead during database mappings | **Improved** |
|
|
19
|
+
|
|
20
|
+
*This is a fully backward-compatible patch. Upgrading is highly recommended to benefit from the performance improvements.* `npm update @mlagie/sql-connector`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 🛡️ Release v2.0.3 — Security Overhaul: Prepared Statements (`.execute`)
|
|
2
|
+
|
|
3
|
+
This major patch introduces a complete migration from raw `.query()` to parameterized Prepared Statements (`.execute()`) for all data-driven database operations, offering definitive protection against SQL Injection.
|
|
4
|
+
|
|
5
|
+
## What Was Improved
|
|
6
|
+
|
|
7
|
+
* **Prepared Statements Migration:** Replaced `.query()` with `.execute()` in critical data-handling pipelines (`save`, `generate_uuid`, `updateOne`, `deleteOne`).
|
|
8
|
+
* **Native Value Escaping:** Transitioned from manual string manipulation/escaping functions to the database driver's native binary protocol placeholder mechanism (`?`).
|
|
9
|
+
* **Bulletproof Security:** User inputs are now strictly bound as parameters, ensuring they are never interpreted as executable SQL commands by the database server.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Component Impact
|
|
14
|
+
|
|
15
|
+
| Impacted Area | Description | Status |
|
|
16
|
+
| :--- | :--- | :--- |
|
|
17
|
+
| **Model.js** | Upgraded `save` and `generate_uuid` to use native placeholder bindings | **Secured** |
|
|
18
|
+
| **ModelInstance.js** | Rewritten `updateOne`, `delete`, and `deleteOne` to prevent unsafe raw query concatenation | **Secured** |
|
|
19
|
+
|
|
20
|
+
*Upgrading is highly recommended for all environments handling user-supplied data.* `npm update @mlagie/sql-connector`
|
package/src/models/Model.js
CHANGED
|
@@ -7,6 +7,7 @@ const { ModelInstance } = require("./ModelInstance");
|
|
|
7
7
|
const { buildSelect, buildQueryParts } = require("../utils/buildQuery");
|
|
8
8
|
const util = require("util");
|
|
9
9
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
10
|
+
const { escapeIdentifier, escapeIdentifierList } = require("../utils/sql");
|
|
10
11
|
|
|
11
12
|
function getFieldType(field) {
|
|
12
13
|
if (typeof field === "object") {
|
|
@@ -54,44 +55,6 @@ function formatDefaultSql(defaultValue, fieldType) {
|
|
|
54
55
|
return `DEFAULT "${String(defaultValue).replace(/"/g, '\\"')}"`;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
function normalizeDefaultValue(defaultValue, fieldType) {
|
|
58
|
-
if (defaultValue === undefined) return null;
|
|
59
|
-
if (defaultValue === null) return "null";
|
|
60
|
-
|
|
61
|
-
if (typeof defaultValue === "function") {
|
|
62
|
-
if (isDateLikeType(fieldType)) return "current_timestamp";
|
|
63
|
-
return normalizeDefaultValue(defaultValue(), fieldType);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (defaultValue instanceof Date) {
|
|
67
|
-
return defaultValue.toISOString().slice(0, 19).replace("T", " ");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (isSqlTemporalDefault(defaultValue)) return "current_timestamp";
|
|
71
|
-
if (typeof defaultValue === "boolean") return defaultValue ? "1" : "0";
|
|
72
|
-
if (typeof defaultValue === "object") return JSON.stringify(defaultValue);
|
|
73
|
-
|
|
74
|
-
return String(defaultValue);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeDbDefaultValue(defaultValue) {
|
|
78
|
-
if (defaultValue === null || defaultValue === undefined) return null;
|
|
79
|
-
|
|
80
|
-
const normalizedValue = String(defaultValue).trim();
|
|
81
|
-
if (/^current_timestamp(\(\))?$/i.test(normalizedValue)) return "current_timestamp";
|
|
82
|
-
if (/^now\(\)$/i.test(normalizedValue)) return "current_timestamp";
|
|
83
|
-
return normalizedValue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function generateValueSQL(value) {
|
|
87
|
-
return value.map(item => {
|
|
88
|
-
if (item === null) return 'NULL';
|
|
89
|
-
if (typeof item === "string") return `"${item.replace(/"/g, '\\"')}"`;
|
|
90
|
-
if (typeof item === "object" && item !== null) return `"${item}"`;
|
|
91
|
-
return item;
|
|
92
|
-
}).join(", ");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
58
|
const reservedKeywords = ['ADD', 'ALL', 'ALTER', 'AND', 'AS', 'ASC', 'BETWEEN', 'BY', 'CASE', 'CHECK', 'COLUMN', 'CONSTRAINT', 'CREATE', 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'DEFAULT', 'DELETE', 'DESC', 'DISTINCT', 'DROP', 'ELSE', 'END', 'ESCAPE', 'EXCEPT', 'EXISTS', 'FOR', 'FOREIGN', 'FROM', 'FULL', 'GROUP', 'HAVING', 'IN', 'INNER', 'INSERT', 'INTERSECT', 'INTO', 'IS', 'JOIN', 'LEFT', 'LIKE', 'LIMIT', 'NOT', 'NULL', 'ON', 'OR', 'ORDER', 'OUTER', 'PRIMARY', 'REFERENCES', 'RIGHT', 'SELECT', 'SET', 'SOME', 'TABLE', 'THEN', 'UNION', 'UNIQUE', 'UPDATE', 'VALUES', 'WHEN', 'WHERE'];
|
|
96
59
|
|
|
97
60
|
/**
|
|
@@ -121,7 +84,7 @@ function getColumnDefinition(fieldName, field) {
|
|
|
121
84
|
}
|
|
122
85
|
|
|
123
86
|
const fieldType = getFieldType(field);
|
|
124
|
-
let colDef
|
|
87
|
+
let colDef;
|
|
125
88
|
|
|
126
89
|
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
127
90
|
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
@@ -138,7 +101,7 @@ function getColumnDefinition(fieldName, field) {
|
|
|
138
101
|
if (field.auto_increment) colDef += ' AUTO_INCREMENT';
|
|
139
102
|
if (field.primary_key) colDef += ' PRIMARY KEY';
|
|
140
103
|
if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
|
|
141
|
-
return `${fieldName} ${colDef}`;
|
|
104
|
+
return `${escapeIdentifier(fieldName)} ${colDef}`;
|
|
142
105
|
}
|
|
143
106
|
|
|
144
107
|
/**
|
|
@@ -166,9 +129,9 @@ class Model {
|
|
|
166
129
|
* @returns {Promise<void>}
|
|
167
130
|
*/
|
|
168
131
|
static async syncAllTables() {
|
|
169
|
-
// Dépendances : {table: [tables dont elle dépend]}
|
|
170
132
|
const dependencies = {};
|
|
171
133
|
const modelMap = {};
|
|
134
|
+
|
|
172
135
|
for (const model of Model.pendingModels) {
|
|
173
136
|
modelMap[model.name] = model;
|
|
174
137
|
dependencies[model.name] = [];
|
|
@@ -181,8 +144,6 @@ class Model {
|
|
|
181
144
|
}
|
|
182
145
|
|
|
183
146
|
const conn = getConnexion();
|
|
184
|
-
const [dbTablesRows] = await conn.promise().query("SHOW TABLES");
|
|
185
|
-
const dbTables = dbTablesRows.map(row => Object.values(row)[0]);
|
|
186
147
|
|
|
187
148
|
const sorted = [];
|
|
188
149
|
const visited = {};
|
|
@@ -205,7 +166,7 @@ class Model {
|
|
|
205
166
|
const model = getSafe(modelMap, table);
|
|
206
167
|
|
|
207
168
|
try {
|
|
208
|
-
await conn.promise().
|
|
169
|
+
await conn.promise().execute(model.generateCreateTableStatement(model.schema.schemaDict));
|
|
209
170
|
await logs(`The table ${model.name} has been created or already exists`);
|
|
210
171
|
} catch (err) {
|
|
211
172
|
error(`Error creating table: ${err} with table name: ${model.name}`);
|
|
@@ -249,7 +210,7 @@ class Model {
|
|
|
249
210
|
error("Error: Invalid table name. Please choose a different name that is not a reserved keyword in SQL_request");
|
|
250
211
|
return;
|
|
251
212
|
}
|
|
252
|
-
return `CREATE TABLE IF NOT EXISTS ${this.name} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
|
|
213
|
+
return `CREATE TABLE IF NOT EXISTS ${escapeIdentifier(this.name)} (${columns.join(', ')}${foreignKey.length > 0 ? ", " + foreignKey.join(', ') : ""}) ENGINE=InnoDB`;
|
|
253
214
|
}
|
|
254
215
|
|
|
255
216
|
getRecordData() {
|
|
@@ -272,10 +233,10 @@ class Model {
|
|
|
272
233
|
*/
|
|
273
234
|
async save(data) {
|
|
274
235
|
const keys = Object.keys(data);
|
|
275
|
-
const sql_request = `INSERT INTO ${this.name} (${keys
|
|
236
|
+
const sql_request = `INSERT INTO ${escapeIdentifier(this.name)} (${escapeIdentifierList(keys)}) VALUES (${keys.map(() => "?").join(", ")})`;
|
|
276
237
|
|
|
277
238
|
try {
|
|
278
|
-
const result = await getConnexion().promise().
|
|
239
|
+
const result = await getConnexion().promise().execute(sql_request, Object.values(data));
|
|
279
240
|
return result[0];
|
|
280
241
|
} catch (err) {
|
|
281
242
|
error(`Error inserting data into ${this.name}: ${err}`);
|
|
@@ -312,9 +273,9 @@ class Model {
|
|
|
312
273
|
if (typeof item === 'string') {
|
|
313
274
|
if (!item.includes('.')) {
|
|
314
275
|
if (item.startsWith('name')) {
|
|
315
|
-
return `${join.table}.${item}`;
|
|
276
|
+
return `${escapeIdentifier(join.table)}.${escapeIdentifier(item)}`;
|
|
316
277
|
}
|
|
317
|
-
return `${this.name}.${item}`;
|
|
278
|
+
return `${escapeIdentifier(this.name)}.${escapeIdentifier(item)}`;
|
|
318
279
|
}
|
|
319
280
|
}
|
|
320
281
|
else if (typeof item === 'object') {
|
|
@@ -323,13 +284,13 @@ class Model {
|
|
|
323
284
|
if (Array.isArray(getSafe(item, key))) {
|
|
324
285
|
setSafe(item, key, getSafe(item, key).map((param, index) => {
|
|
325
286
|
if (index === 0 && typeof param === 'string' && !param.includes('.')) {
|
|
326
|
-
return `${this.name}.${param}`;
|
|
287
|
+
return `${escapeIdentifier(this.name)}.${escapeIdentifier(param)}`;
|
|
327
288
|
}
|
|
328
289
|
return param;
|
|
329
290
|
}));
|
|
330
291
|
}
|
|
331
292
|
else if (typeof getSafe(item, key) === 'string' && !getSafe(item, key).includes('.')) {
|
|
332
|
-
setSafe(item, key, `${this.name}.${getSafe(item, key)}`);
|
|
293
|
+
setSafe(item, key, `${escapeIdentifier(this.name)}.${escapeIdentifier(getSafe(item, key))}`);
|
|
333
294
|
}
|
|
334
295
|
}
|
|
335
296
|
return item;
|
|
@@ -337,23 +298,22 @@ class Model {
|
|
|
337
298
|
}
|
|
338
299
|
let joinClause = "";
|
|
339
300
|
if (join && join.table && join.on) {
|
|
340
|
-
joinClause = ` INNER JOIN ${join.table} ON ${join.on}`;
|
|
301
|
+
joinClause = ` INNER JOIN ${escapeIdentifier(join.table)} ON ${join.on}`;
|
|
341
302
|
}
|
|
342
303
|
|
|
343
|
-
const query = `SELECT ${buildSelect(select)} FROM ${this.name}${joinClause} ${buildQueryParts(options)}`;
|
|
304
|
+
const query = `SELECT ${buildSelect(select)} FROM ${escapeIdentifier(this.name)}${joinClause} ${buildQueryParts(options)}`;
|
|
344
305
|
|
|
345
|
-
|
|
346
|
-
await getConnexion().promise().
|
|
347
|
-
|
|
348
|
-
if (!rows || rows.length === 0) return resolve([]);
|
|
306
|
+
try {
|
|
307
|
+
const result = await getConnexion().promise().execute(query);
|
|
308
|
+
const rows = result && Array.isArray(result) ? result[0] : result;
|
|
349
309
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
310
|
+
if (!rows || rows.length === 0) return [];
|
|
311
|
+
|
|
312
|
+
return rows.map(row => new ModelInstance(this.name, row, this.schema));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
error(`Error executing auto-prefixed find: ${err}`);
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
357
317
|
}
|
|
358
318
|
|
|
359
319
|
/**
|
|
@@ -362,7 +322,7 @@ class Model {
|
|
|
362
322
|
* @returns {Promise<ModelInstance|number>} - A promise that resolves to a `ModelInstance` if a record is found, or `0` if no records match the filter.
|
|
363
323
|
*/
|
|
364
324
|
async count(filter) {
|
|
365
|
-
return this.customRequest(`SELECT COUNT(*) as count FROM ${this.name} ${filter != undefined ? `WHERE ${generateCondition(formatObject(filter))}` : ""}`, "count");
|
|
325
|
+
return this.customRequest(`SELECT COUNT(*) as count FROM ${escapeIdentifier(this.name)} ${filter != undefined ? `WHERE ${generateCondition(formatObject(filter))}` : ""}`, "count");
|
|
366
326
|
}
|
|
367
327
|
|
|
368
328
|
/**
|
|
@@ -372,16 +332,16 @@ class Model {
|
|
|
372
332
|
* @throws {Error} Throws an error if query execution fails.
|
|
373
333
|
*/
|
|
374
334
|
async customRequest(custom, custom_err_name = "") {
|
|
375
|
-
|
|
376
|
-
await getConnexion().promise().
|
|
377
|
-
if (rows.length == 0) return resolve(0);
|
|
335
|
+
try {
|
|
336
|
+
const rows = await getConnexion().promise().execute(custom);
|
|
378
337
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
});
|
|
384
|
-
|
|
338
|
+
if (rows[0].length == 0) return 0;
|
|
339
|
+
|
|
340
|
+
return new ModelInstance(this.name, rows[0], this.schema);
|
|
341
|
+
} catch (err) {
|
|
342
|
+
error(`Error executing query ${custom_err_name}: ${err}`);
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
385
345
|
}
|
|
386
346
|
|
|
387
347
|
/**
|
|
@@ -393,10 +353,10 @@ class Model {
|
|
|
393
353
|
* @throws {Error} Throws an error if the SQL query fails.
|
|
394
354
|
*/
|
|
395
355
|
async delete(filter) {
|
|
396
|
-
const sql_request = `DELETE FROM ${this.name} WHERE ${generateCondition(formatObject(filter))}`;
|
|
356
|
+
const sql_request = `DELETE FROM ${escapeIdentifier(this.name)} WHERE ${generateCondition(formatObject(filter))}`;
|
|
397
357
|
return new Promise((resolve, reject) => {
|
|
398
|
-
getConnexion().promise().
|
|
399
|
-
if (rows[
|
|
358
|
+
getConnexion().promise().execute(sql_request).then((rows) => {
|
|
359
|
+
if (rows[0].affectedRows === 0) return resolve(0);
|
|
400
360
|
|
|
401
361
|
return resolve(1);
|
|
402
362
|
}).catch((err) => {
|
|
@@ -417,10 +377,10 @@ class Model {
|
|
|
417
377
|
* @returns {Promise<void>} A promise that resolves when the query execution is complete.
|
|
418
378
|
*/
|
|
419
379
|
async dropTable() {
|
|
420
|
-
const sql_request = `DROP TABLE IF EXISTS ${this.name};`;
|
|
380
|
+
const sql_request = `DROP TABLE IF EXISTS ${escapeIdentifier(this.name)};`;
|
|
421
381
|
|
|
422
382
|
return new Promise((resolve, reject) => {
|
|
423
|
-
getConnexion().promise().
|
|
383
|
+
getConnexion().promise().execute(sql_request).then((rows) => {
|
|
424
384
|
console.log(rows);
|
|
425
385
|
}).catch((err) => {
|
|
426
386
|
error(`Error executing query drop: ${err}`);
|
|
@@ -449,11 +409,11 @@ class Model {
|
|
|
449
409
|
* @throws {Error} If there is an error executing the SQL_request query.
|
|
450
410
|
*/
|
|
451
411
|
async generate_uuid(var_uuid = "uuid") {
|
|
452
|
-
const uuid = (await getConnexion().promise().
|
|
453
|
-
const sql_request = `SELECT COUNT(*) FROM ${this.name} WHERE ${var_uuid}=
|
|
412
|
+
const uuid = (await getConnexion().promise().execute("SELECT UUID();"))[0][0]["UUID()"];
|
|
413
|
+
const sql_request = `SELECT COUNT(*) FROM ${escapeIdentifier(this.name)} WHERE ${escapeIdentifier(var_uuid)} = ?;`;
|
|
454
414
|
|
|
455
415
|
return new Promise((resolve, reject) => {
|
|
456
|
-
getConnexion().promise().
|
|
416
|
+
getConnexion().promise().execute(sql_request, [uuid]).then((rows) => {
|
|
457
417
|
if (rows[0][0]['COUNT(*)'] == 0) return resolve(uuid);
|
|
458
418
|
resolve(null);
|
|
459
419
|
}).catch((err) => {
|
|
@@ -3,7 +3,6 @@ const { getConnexion } = require("../db/connexion");
|
|
|
3
3
|
const formatObject = require("../utils/formatObject");
|
|
4
4
|
const generateCondition = require("../utils/generateCondition");
|
|
5
5
|
const util = require("util");
|
|
6
|
-
const { serveur } = require("@mlagie/logger");
|
|
7
6
|
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -45,7 +44,7 @@ class ModelInstance {
|
|
|
45
44
|
get: () => {
|
|
46
45
|
const val = getSafe(row, key);
|
|
47
46
|
if (typeof val === 'string' && val.trim().startsWith('{') && val.trim().endsWith('}')) {
|
|
48
|
-
try { return JSON.parse(val); } catch
|
|
47
|
+
try { return JSON.parse(val); } catch { return val; }
|
|
49
48
|
}
|
|
50
49
|
return val;
|
|
51
50
|
},
|
|
@@ -99,13 +98,15 @@ class ModelInstance {
|
|
|
99
98
|
if (typeof rawRec === 'string') {
|
|
100
99
|
try {
|
|
101
100
|
rawRec = JSON.parse(rawRec);
|
|
102
|
-
} catch
|
|
101
|
+
} catch {
|
|
102
|
+
rawRec = recordsArray;
|
|
103
|
+
}
|
|
103
104
|
}
|
|
104
105
|
const rec = rawRec;
|
|
105
106
|
|
|
106
107
|
const schemaDict = this.schema && this.schema.schemaDict ? this.schema.schemaDict : null;
|
|
107
108
|
if (schemaDict) {
|
|
108
|
-
const pkKeys = Object.entries(schemaDict).filter(([
|
|
109
|
+
const pkKeys = Object.entries(schemaDict).filter(([, v]) => v && v.primary_key === true).map(([k]) => k);
|
|
109
110
|
if (pkKeys.length > 0) {
|
|
110
111
|
const pkObj = {};
|
|
111
112
|
for (const k of pkKeys) {
|
|
@@ -115,16 +116,17 @@ class ModelInstance {
|
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
if (!whereClause) whereClause = generateCondition(formatObject(rec), false, this.schema);
|
|
118
|
-
} catch
|
|
119
|
-
|
|
119
|
+
} catch {
|
|
120
|
+
const originalFallbackRec = this.getRecordData();
|
|
121
|
+
let fallbackRec = originalFallbackRec;
|
|
120
122
|
if (Array.isArray(fallbackRec)) fallbackRec = fallbackRec[0];
|
|
121
|
-
if (typeof fallbackRec === 'string') { try { fallbackRec = JSON.parse(fallbackRec); } catch
|
|
123
|
+
if (typeof fallbackRec === 'string') { try { fallbackRec = JSON.parse(fallbackRec); } catch { fallbackRec = originalFallbackRec; } }
|
|
122
124
|
whereClause = generateCondition(formatObject(fallbackRec), false, this.schema);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
const sql_request = `UPDATE ${this.name} SET ${setClause} WHERE ${whereClause}`;
|
|
126
128
|
|
|
127
|
-
const [result] = await getConnexion().promise().
|
|
129
|
+
const [result] = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
128
130
|
error(`Error executing query updateOne: ${err}`);
|
|
129
131
|
throw err;
|
|
130
132
|
});
|
|
@@ -152,17 +154,12 @@ class ModelInstance {
|
|
|
152
154
|
async delete(filter) {
|
|
153
155
|
const sql_request = `DELETE FROM ${this.name} WHERE ${generateCondition(formatObject(filter))}`;
|
|
154
156
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (rows[1] != undefined) return resolve(0);
|
|
159
|
-
|
|
160
|
-
resolve(1);
|
|
161
|
-
}).catch((err) => {
|
|
162
|
-
error(`Error executing query delete: ${err}`);
|
|
163
|
-
return 0;
|
|
164
|
-
});
|
|
157
|
+
const rows = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
158
|
+
error(`Error executing query delete: ${err}`);
|
|
159
|
+
throw err;
|
|
165
160
|
});
|
|
161
|
+
|
|
162
|
+
return rows[0].affectedRows === 0 ? 0 : 1;
|
|
166
163
|
}
|
|
167
164
|
|
|
168
165
|
/**
|
|
@@ -173,16 +170,12 @@ class ModelInstance {
|
|
|
173
170
|
async deleteOne() {
|
|
174
171
|
const sql_request = `DELETE FROM ${this.name} WHERE ${generateCondition(formatObject(this.getRecordData()))}`;
|
|
175
172
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
resolve(1);
|
|
181
|
-
}).catch((err) => {
|
|
182
|
-
error(`Error executing query deleteOne: ${err}`);
|
|
183
|
-
return 0;
|
|
184
|
-
});
|
|
173
|
+
const rows = await getConnexion().promise().execute(sql_request).catch((err) => {
|
|
174
|
+
error(`Error executing query deleteOne: ${err}`);
|
|
175
|
+
throw err;
|
|
185
176
|
});
|
|
177
|
+
|
|
178
|
+
return rows[0].affectedRows === 0 ? 0 : 1;
|
|
186
179
|
}
|
|
187
180
|
|
|
188
181
|
/**
|
|
@@ -192,16 +185,14 @@ class ModelInstance {
|
|
|
192
185
|
* @throws {Error} Throws an error if query execution fails.
|
|
193
186
|
*/
|
|
194
187
|
async customRequest(custom) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
resolve(new ModelInstance(this.name, rows, this.schema)).data;
|
|
200
|
-
}).catch((err) => {
|
|
201
|
-
error(`Error executing query: ${err}`);
|
|
202
|
-
return;
|
|
203
|
-
});
|
|
188
|
+
const rows = await getConnexion().promise().execute(custom).catch((err) => {
|
|
189
|
+
error(`Error executing query: ${err}`);
|
|
190
|
+
throw err;
|
|
204
191
|
});
|
|
192
|
+
|
|
193
|
+
if (rows[0].length == 0) return 0;
|
|
194
|
+
|
|
195
|
+
return new ModelInstance(this.name, rows[0], this.schema).data;
|
|
205
196
|
}
|
|
206
197
|
}
|
|
207
198
|
|
package/src/utils/buildQuery.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
const formatObject = require("./formatObject");
|
|
2
2
|
const generateCondition = require("./generateCondition");
|
|
3
|
+
const { escapeIdentifier, escapeOrderDirection, escapeValue } = require("./sql");
|
|
3
4
|
|
|
4
5
|
function buildField(field) {
|
|
5
6
|
if (typeof field === 'string') {
|
|
6
|
-
return
|
|
7
|
+
if (field === "*") return "*";
|
|
8
|
+
return escapeIdentifier(field);
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
let sql = '';
|
|
10
12
|
|
|
11
13
|
if (field.sum)
|
|
12
|
-
sql = `SUM(${field.sum})`;
|
|
14
|
+
sql = `SUM(${escapeIdentifier(field.sum)})`;
|
|
13
15
|
else if (field.dateFormat) {
|
|
14
16
|
const [col, format] = field.dateFormat;
|
|
15
|
-
sql = `DATE_FORMAT(${col},
|
|
17
|
+
sql = `DATE_FORMAT(${escapeIdentifier(col)}, ${escapeValue(format)})`;
|
|
16
18
|
}
|
|
17
19
|
else if (field.col)
|
|
18
|
-
sql = field.col;
|
|
20
|
+
sql = escapeIdentifier(field.col);
|
|
19
21
|
|
|
20
22
|
if (field.as)
|
|
21
|
-
sql += ` AS ${field.as}`;
|
|
23
|
+
sql += ` AS ${escapeIdentifier(field.as)}`;
|
|
22
24
|
else if (field.sum)
|
|
23
|
-
sql += ` AS ${field.sum}`;
|
|
25
|
+
sql += ` AS ${escapeIdentifier(field.sum)}`;
|
|
24
26
|
return sql;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -39,30 +41,33 @@ function buildQueryParts(options) {
|
|
|
39
41
|
|
|
40
42
|
if (options.where) {
|
|
41
43
|
if (typeof options.where === 'string') {
|
|
42
|
-
|
|
44
|
+
throw new Error("Raw string WHERE clauses are not allowed. Pass an object filter instead.");
|
|
43
45
|
} else {
|
|
44
46
|
parts.push(`WHERE ${generateCondition(formatObject(options.where))}`);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
if (options.groupBy) {
|
|
49
|
-
parts.push(`GROUP BY ${options.groupBy.join(', ')}`);
|
|
51
|
+
parts.push(`GROUP BY ${options.groupBy.map(group => escapeIdentifier(group)).join(', ')}`);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
if (options.having) {
|
|
53
|
-
|
|
55
|
+
throw new Error("Raw string HAVING clauses are not allowed. Use a structured filter instead.");
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (options.orderBy) {
|
|
57
59
|
const order = options.orderBy.map(o =>
|
|
58
60
|
typeof o === 'string'
|
|
59
|
-
? o
|
|
60
|
-
: `${o.field} ${o.direction || 'ASC'}`
|
|
61
|
+
? escapeIdentifier(o)
|
|
62
|
+
: `${escapeIdentifier(o.field)} ${escapeOrderDirection(o.direction || 'ASC')}`
|
|
61
63
|
);
|
|
62
64
|
parts.push(`ORDER BY ${order.join(', ')}`);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
if (options.limit) {
|
|
68
|
+
if (!Number.isInteger(options.limit) || options.limit < 0) {
|
|
69
|
+
throw new Error("Invalid LIMIT value");
|
|
70
|
+
}
|
|
66
71
|
parts.push(`LIMIT ${options.limit}`);
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -2,7 +2,7 @@ const { getSafe, setSafe } = require("./security/safe");
|
|
|
2
2
|
|
|
3
3
|
module.exports = function (obj) {
|
|
4
4
|
for (const key in obj) {
|
|
5
|
-
if (
|
|
5
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
6
6
|
const value = getSafe(obj, key);
|
|
7
7
|
if (value instanceof Date) {
|
|
8
8
|
// convert Date to MySQL DATETIME (no timezone)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { getSafe } = require("./security/safe");
|
|
2
|
+
const { escapeIdentifier, escapeValue } = require("./sql");
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Generates an SQL_request condition from a filter object.
|
|
@@ -32,6 +33,7 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
32
33
|
if (uniqueKeys.length > 0) {
|
|
33
34
|
return uniqueKeys.map(key => {
|
|
34
35
|
let value = getSafe(filter, key);
|
|
36
|
+
const escapedKey = escapeIdentifier(key);
|
|
35
37
|
// normalize strings that may contain surrounding quotes or escaped quotes
|
|
36
38
|
if (typeof value === 'string') {
|
|
37
39
|
value = value.trim();
|
|
@@ -41,19 +43,19 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
41
43
|
value = value.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
42
44
|
}
|
|
43
45
|
if (Array.isArray(value)) {
|
|
44
|
-
return `${
|
|
46
|
+
return `${escapedKey} IN (${value.map(v => escapeValue(v)).join(", ")})`;
|
|
45
47
|
}
|
|
46
48
|
if (typeof value === "object" || (typeof value === "string" && value.trim().startsWith("{") && value.trim().endsWith("}"))) {
|
|
47
49
|
const jsonVal = typeof value === "string" ? value : JSON.stringify(value);
|
|
48
|
-
return `JSON_CONTAINS(${
|
|
50
|
+
return `JSON_CONTAINS(${escapedKey}, ${escapeValue(jsonVal)})`;
|
|
49
51
|
}
|
|
50
|
-
if (value === null || value === "null") return `${
|
|
52
|
+
if (value === null || value === "null") return `${escapedKey} IS NULL`;
|
|
51
53
|
// if string looks like an ISO datetime, convert to MySQL DATETIME format
|
|
52
54
|
if (typeof value === 'string' && /T/.test(value)) {
|
|
53
55
|
let val = value.replace(/\.\d+Z$/,'').replace(/Z$/,'').replace('T',' ');
|
|
54
|
-
return `${
|
|
56
|
+
return `${escapedKey} = ${escapeValue(val)}`;
|
|
55
57
|
}
|
|
56
|
-
return `${
|
|
58
|
+
return `${escapedKey} = ${escapeValue(value)}`;
|
|
57
59
|
}).join(" AND ");
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -61,6 +63,7 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
61
63
|
// Comportement par défaut
|
|
62
64
|
const conditions = filteredKeys.map((key, index) => {
|
|
63
65
|
let value = getSafe(filteredValues, index);
|
|
66
|
+
const escapedKey = escapeIdentifier(key);
|
|
64
67
|
|
|
65
68
|
if (typeof value === 'string') {
|
|
66
69
|
value = value.trim();
|
|
@@ -71,17 +74,17 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
if (Array.isArray(value)) {
|
|
74
|
-
return `${
|
|
77
|
+
return `${escapedKey} IN (${value.map(v => escapeValue(v)).join(", ")})`;
|
|
75
78
|
}
|
|
76
79
|
if (typeof value === "object" || (typeof value === "string" && value.trim().startsWith("{") && value.trim().endsWith("}"))) {
|
|
77
80
|
const jsonVal = typeof value === "string" ? value : JSON.stringify(value);
|
|
78
81
|
if (isUpdate) {
|
|
79
|
-
return `${
|
|
82
|
+
return `${escapedKey} = ${escapeValue(jsonVal)}`;
|
|
80
83
|
}
|
|
81
|
-
return `JSON_CONTAINS(${
|
|
84
|
+
return `JSON_CONTAINS(${escapedKey}, ${escapeValue(jsonVal)})`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
if ((value === null || value === "null") && isUpdate == false) return `${
|
|
87
|
+
if ((value === null || value === "null") && isUpdate == false) return `${escapedKey} IS NULL`;
|
|
85
88
|
|
|
86
89
|
// handle date-like strings when schema tells us the field is temporal
|
|
87
90
|
const fieldDef = schema && schema.schemaDict ? getSafe(schema.schemaDict, key) : null;
|
|
@@ -102,9 +105,9 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
102
105
|
if (isDateLike && /T/.test(val)) {
|
|
103
106
|
val = val.replace(/\.\d+Z$/,'').replace(/Z$/,'').replace('T',' ');
|
|
104
107
|
}
|
|
105
|
-
return `${
|
|
108
|
+
return `${escapedKey} = ${escapeValue(val)}`;
|
|
106
109
|
}
|
|
107
|
-
return `${
|
|
110
|
+
return `${escapedKey} = ${escapeValue(value)}`;
|
|
108
111
|
}).join(` ${isUpdate == false ? "AND" : ","} `);
|
|
109
112
|
return conditions;
|
|
110
113
|
}
|
package/src/utils/sql.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { escape, escapeId } = require("mysql2");
|
|
2
|
+
|
|
3
|
+
const SAFE_IDENTIFIER = /^[A-Za-z0-9_]+$/;
|
|
4
|
+
|
|
5
|
+
function escapeIdentifier(identifier) {
|
|
6
|
+
if (identifier === "*") return "*";
|
|
7
|
+
|
|
8
|
+
if (typeof identifier !== "string" || identifier.length === 0) {
|
|
9
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return identifier.split(".").map(part => {
|
|
13
|
+
if (part === "*") return "*";
|
|
14
|
+
if (!SAFE_IDENTIFIER.test(part)) {
|
|
15
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
16
|
+
}
|
|
17
|
+
return escapeId(part);
|
|
18
|
+
}).join(".");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function escapeIdentifierList(identifiers) {
|
|
22
|
+
return identifiers.map(identifier => escapeIdentifier(identifier)).join(", ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function escapeValue(value) {
|
|
26
|
+
return escape(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeOrderDirection(direction) {
|
|
30
|
+
const normalized = String(direction ?? "ASC").toUpperCase();
|
|
31
|
+
|
|
32
|
+
if (normalized !== "ASC" && normalized !== "DESC") {
|
|
33
|
+
throw new Error(`Invalid SQL sort direction: ${direction}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
escapeIdentifier,
|
|
41
|
+
escapeIdentifierList,
|
|
42
|
+
escapeOrderDirection,
|
|
43
|
+
escapeValue
|
|
44
|
+
};
|