@mlagie/sql-connector 1.5.0 → 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/workflows/publish.yml +47 -0
- package/README.md +0 -4
- package/docs/fr/README.md +0 -4
- package/eslint.config.mjs +12 -0
- package/index.d.ts +23 -24
- package/package.json +6 -2
- package/releases/2.0.0.md +32 -0
- package/src/models/Model.js +75 -280
- package/src/models/ModelInstance.js +7 -16
- package/src/utils/buildQuery.js +0 -1
- package/src/utils/formatObject.js +6 -4
- package/src/utils/generateCondition.js +8 -6
- package/src/utils/security/safe.js +35 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Publish Package
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check_security:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout GH repository
|
|
13
|
+
uses: actions/checkout@v6
|
|
14
|
+
- name: setup node
|
|
15
|
+
uses: actions/setup-node@v6
|
|
16
|
+
with:
|
|
17
|
+
node-version: 22
|
|
18
|
+
- name: Install deps
|
|
19
|
+
run: npm ci
|
|
20
|
+
- name: Audit deps
|
|
21
|
+
continue-on-error: false
|
|
22
|
+
run: |
|
|
23
|
+
npm audit
|
|
24
|
+
- name: Check security of module
|
|
25
|
+
continue-on-error: false
|
|
26
|
+
run: |
|
|
27
|
+
npm run security
|
|
28
|
+
publish:
|
|
29
|
+
needs: check_security
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment: sqlc
|
|
32
|
+
permissions:
|
|
33
|
+
contents: read
|
|
34
|
+
id-token: write
|
|
35
|
+
|
|
36
|
+
steps:
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
- uses: actions/setup-node@v6
|
|
39
|
+
with:
|
|
40
|
+
node-version: 22
|
|
41
|
+
registry-url: https://registry.npmjs.org
|
|
42
|
+
scope: "@mlagie"
|
|
43
|
+
- name: Update npm and Publish
|
|
44
|
+
run: |
|
|
45
|
+
npm install -g npm@latest
|
|
46
|
+
npm ci
|
|
47
|
+
npm publish --provenance --access public
|
package/README.md
CHANGED
|
@@ -128,13 +128,9 @@ const userSchema = new Schema({
|
|
|
128
128
|
`Model.syncAllTables()` compares JS schemas with the database and applies only meaningful differences.
|
|
129
129
|
|
|
130
130
|
- New columns are added automatically.
|
|
131
|
-
- Removed columns are only dropped with `dangerousSync: true`.
|
|
132
|
-
- Column renames are supported through `oldName`.
|
|
133
|
-
- Orphan tables are backed up to a `backup_*.sql` file before deletion.
|
|
134
131
|
|
|
135
132
|
```javascript
|
|
136
133
|
await Model.syncAllTables();
|
|
137
|
-
await Model.syncAllTables({ dangerousSync: true });
|
|
138
134
|
```
|
|
139
135
|
|
|
140
136
|
Important: do not set both `primary_key: true` and `unique: true` on the same field. A primary key is already unique and not null.
|
package/docs/fr/README.md
CHANGED
|
@@ -93,13 +93,9 @@ const userSchema = new Schema({
|
|
|
93
93
|
`Model.syncAllTables()` compare les schémas JS avec la base et applique uniquement les différences utiles.
|
|
94
94
|
|
|
95
95
|
- Ajout de colonne: automatique.
|
|
96
|
-
- Suppression de colonne: uniquement avec `dangerousSync: true`.
|
|
97
|
-
- Renommage de colonne: possible avec `oldName`.
|
|
98
|
-
- Tables orphelines: sauvegarde avant suppression dans un fichier `backup_*.sql`.
|
|
99
96
|
|
|
100
97
|
```javascript
|
|
101
98
|
await Model.syncAllTables();
|
|
102
|
-
await Model.syncAllTables({ dangerousSync: true });
|
|
103
99
|
```
|
|
104
100
|
|
|
105
101
|
Point important: ne combinez pas `primary_key: true` et `unique: true` sur le même champ. Une clé primaire est déjà unique et non nulle.
|
package/index.d.ts
CHANGED
|
@@ -96,27 +96,26 @@ export class Model {
|
|
|
96
96
|
constructor(name: string, schema: Schema);
|
|
97
97
|
/**
|
|
98
98
|
* Creates all tables in the correct order based on foreign keys.
|
|
99
|
-
* @param dangerousSync default false
|
|
100
99
|
* @returns {Promise<void>}
|
|
101
100
|
*/
|
|
102
|
-
static
|
|
101
|
+
static syncAllTables(): Promise<void>;
|
|
103
102
|
/**
|
|
104
103
|
* Saves data to the database table.
|
|
105
104
|
* @param {Object} data The data to insert into the table.
|
|
106
105
|
* @returns {Promise<Object>} A promise that resolves with the result of the insertion.
|
|
107
106
|
* @throws {Error} Throws an error if the insert fails.
|
|
108
107
|
*/
|
|
109
|
-
|
|
108
|
+
save(data: Record<string, any>): Promise<any>;
|
|
110
109
|
/**
|
|
111
|
-
*
|
|
112
|
-
* @param {Object} [options] -
|
|
113
|
-
* @param {string[]} [options.select] -
|
|
114
|
-
* @param {Object} [options.where] -
|
|
115
|
-
* @param {Array} [options.order] -
|
|
116
|
-
* @param {number} [options.limit] -
|
|
110
|
+
* Retrieves multiple rows from the table.
|
|
111
|
+
* @param {Object} [options] - Query options (attributes, where, order, limit).
|
|
112
|
+
* @param {string[]} [options.select] - Fields to return.
|
|
113
|
+
* @param {Object} [options.where] - Filters (key/value).
|
|
114
|
+
* @param {Array} [options.order] - Example: [['points', 'DESC']]
|
|
115
|
+
* @param {number} [options.limit] - Result limit.
|
|
117
116
|
* @returns {Promise<Array<Object>>}
|
|
118
117
|
*/
|
|
119
|
-
static
|
|
118
|
+
static find(options?: {
|
|
120
119
|
select?: string[];
|
|
121
120
|
where?: Record<string, any>;
|
|
122
121
|
order?: [string, string][];
|
|
@@ -127,25 +126,25 @@ export class Model {
|
|
|
127
126
|
* @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.
|
|
128
127
|
* @returns {Promise<ModelInstance|number>} - A promise that resolves to a `ModelInstance` if a record is found, or `0` if no records match the filter.
|
|
129
128
|
*/
|
|
130
|
-
|
|
129
|
+
count(filter?: Record<string, any>): Promise<any>;
|
|
131
130
|
/**
|
|
132
131
|
* Runs a custom SQL_request query.
|
|
133
132
|
* @param {string} custom The custom SQL_request query to execute.
|
|
134
133
|
* @returns {Promise<void>} A promise that resolves when the query is executed.
|
|
135
134
|
* @throws {Error} Throws an error if query execution fails.
|
|
136
135
|
*/
|
|
137
|
-
|
|
136
|
+
customRequest(custom: string): Promise<any>;
|
|
138
137
|
/**
|
|
139
|
-
*
|
|
138
|
+
* Deletes a record from the SQL table corresponding to the provided filter.
|
|
140
139
|
*
|
|
141
140
|
* @async
|
|
142
141
|
* @function delete
|
|
143
|
-
* @param {Object} filter -
|
|
144
|
-
* @returns {Promise<number>}
|
|
145
|
-
*
|
|
146
|
-
* @throws {Error}
|
|
142
|
+
* @param {Object} filter - An object representing the filter conditions for the deletion.
|
|
143
|
+
* @returns {Promise<number>} A promise that resolves to 0 if no rows were deleted,
|
|
144
|
+
* or an instance of ModelInstance representing the deleted row.
|
|
145
|
+
* @throws {Error} Throws an error if the SQL query fails.
|
|
147
146
|
*/
|
|
148
|
-
|
|
147
|
+
delete(filter: Record<string, any>): Promise<number | ModelInstance>;
|
|
149
148
|
/**
|
|
150
149
|
* Asynchronously drops a table if it exists in the database.
|
|
151
150
|
*
|
|
@@ -156,7 +155,7 @@ export class Model {
|
|
|
156
155
|
*
|
|
157
156
|
* @returns {Promise<void>} A promise that resolves when the query execution is complete.
|
|
158
157
|
*/
|
|
159
|
-
|
|
158
|
+
dropTable(): Promise<void>;
|
|
160
159
|
/**
|
|
161
160
|
* Generates a unique UUID for the current model.
|
|
162
161
|
*
|
|
@@ -176,7 +175,7 @@ export class Model {
|
|
|
176
175
|
*
|
|
177
176
|
* @throws {Error} If there is an error executing the SQL_request query.
|
|
178
177
|
*/
|
|
179
|
-
|
|
178
|
+
generate_uuid(var_uuid?: string): Promise<string | null>;
|
|
180
179
|
}
|
|
181
180
|
|
|
182
181
|
/**
|
|
@@ -195,27 +194,27 @@ export class ModelInstance {
|
|
|
195
194
|
* @returns {int} A promise that resolves with updated data.
|
|
196
195
|
* @throws {Error} Throws an error if the update fails.
|
|
197
196
|
*/
|
|
198
|
-
|
|
197
|
+
updateOne(model: Record<string, any>): Promise<number>;
|
|
199
198
|
/**
|
|
200
199
|
* Deletes a single entry in the database table.
|
|
201
200
|
* @param {Object} model An object containing the key-value pairs to use for deletion.
|
|
202
201
|
* @returns {Promise<Object>} A promise that resolves with the data deleted.
|
|
203
202
|
* @throws {Error} Throws an error if the deletion fails.
|
|
204
203
|
*/
|
|
205
|
-
|
|
204
|
+
delete(filter: Record<string, any>): Promise<number | ModelInstance>;
|
|
206
205
|
/**
|
|
207
206
|
* Deletes a single entry in the database table based on the instance data.
|
|
208
207
|
* @returns {Promise<number>} A promise that resolves to the number of rows deleted.
|
|
209
208
|
* @throws {Error} Throws an error if the deletion fails.
|
|
210
209
|
*/
|
|
211
|
-
|
|
210
|
+
deleteOne(): Promise<number>;
|
|
212
211
|
/**
|
|
213
212
|
* Runs a custom SQL_request query.
|
|
214
213
|
* @param {string} custom The custom SQL_request query to execute.
|
|
215
214
|
* @returns {Promise<void>} A promise that resolves when the query is executed.
|
|
216
215
|
* @throws {Error} Throws an error if query execution fails.
|
|
217
216
|
*/
|
|
218
|
-
|
|
217
|
+
customRequest(custom: string): Promise<any>;
|
|
219
218
|
}
|
|
220
219
|
|
|
221
220
|
export const client: Record<string, any>;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mlagie/sql-connector",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": {
|
|
7
|
-
"
|
|
7
|
+
"security": "npx eslint . --max-warnings 0"
|
|
8
8
|
},
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -38,5 +38,9 @@
|
|
|
38
38
|
"@mlagie/logger": "1.0.2",
|
|
39
39
|
"glob": "^13.0.6",
|
|
40
40
|
"mysql2": "3.22.5"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"eslint": "^10.6.0",
|
|
44
|
+
"eslint-plugin-security": "^4.0.1"
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# **Release v2.0.0 — Security Hardening & Prototype Pollution Prevention**
|
|
2
|
+
|
|
3
|
+
This release focuses heavily on `security improvements` across the ORM's core model management and data access layers. We have successfully mitigated potential security vulnerabilities regarding dynamic property access and file path operations, satisfying `eslint-plugin-security` analysis.
|
|
4
|
+
|
|
5
|
+
## **Key Changes & Security Fixes**
|
|
6
|
+
|
|
7
|
+
### **1. Prototype Pollution & Object Injection Prevention**
|
|
8
|
+
|
|
9
|
+
- **Problem**: Using dynamic bracket notations like obj[key] where key originates from user inputs or database schemas exposed the application to **Object Injection** and **Prototype Pollution** (allowing attackers to alter critical global properties via keys like `__proto__` or `constructor`).
|
|
10
|
+
|
|
11
|
+
- **Fix**: Introduced a centralized `safe.js` security utility leveraging `Reflect.get()` and `Reflect.set()` coupled with a strict property blacklist (`__proto__`, `constructor`, `prototype`).
|
|
12
|
+
- Migrated all dynamic model lookups and topological sorting dictionaries in `Model.js` to safe wrappers.
|
|
13
|
+
- Secured dynamic getters and setters mapped to data columns in `ModelInstance.js`.
|
|
14
|
+
|
|
15
|
+
### **2. Path Traversal & Safe Database Backups**
|
|
16
|
+
|
|
17
|
+
- **Problem**: Dynamically naming automated backup files (**.sql**) based on database table names without validation triggered `detect-non-literal-fs-filename` warnings, opening up potential `Path Traversal` vector concerns.
|
|
18
|
+
|
|
19
|
+
- **Fix**: Implemented string sanitization via regex (`/[^a-zA-Z0-9_]/g`) on table-driven variables prior to passing arguments to standard `fs` sync methods (`writeFileSync`, `readFileSync`, `unlinkSync`, `renameSync`).
|
|
20
|
+
- Fully resolved all local `fs` filename security alerts.
|
|
21
|
+
|
|
22
|
+
### **3. General Code Refactoring**
|
|
23
|
+
|
|
24
|
+
- Replaced native object literals `{}` with prototype-less structures `(Object.create(null))` for configuration mappings and dependency graph resolution arrays to prevent unexpected inheritance bugs.
|
|
25
|
+
|
|
26
|
+
## **Component Impact**
|
|
27
|
+
|
|
28
|
+
| File | Changes Made | Warning Cleared |
|
|
29
|
+
|-------------------------------|-------------------------------------------------------------------|-------------------------------------------------------------|
|
|
30
|
+
| `src/utils/security/safe.js` | Created utility with `Reflect` API + validation blacklist | None (Fully clean) |
|
|
31
|
+
| `src/models/Model.js` | Patched `syncAllTables`, topological sort, and `fs` operations | `detect-object-injection`, `detect-non-literal-fs-filename` |
|
|
32
|
+
| `src/models/ModelInstance.js` | Secured active schema columns maps inside constructor proxies | `detect-object-injection` |
|
package/src/models/Model.js
CHANGED
|
@@ -3,12 +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");
|
|
10
7
|
const { buildSelect, buildQueryParts } = require("../utils/buildQuery");
|
|
11
8
|
const util = require("util");
|
|
9
|
+
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
12
10
|
|
|
13
11
|
function getFieldType(field) {
|
|
14
12
|
if (typeof field === "object") {
|
|
@@ -99,8 +97,8 @@ const reservedKeywords = ['ADD', 'ALL', 'ALTER', 'AND', 'AS', 'ASC', 'BETWEEN',
|
|
|
99
97
|
/**
|
|
100
98
|
* Checks if a table name is a reserved keyword.
|
|
101
99
|
*
|
|
102
|
-
* @param {string} tableName
|
|
103
|
-
* @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`.
|
|
104
102
|
*
|
|
105
103
|
* @example
|
|
106
104
|
* const isReserved = ifReservedKeywords('SELECT');
|
|
@@ -129,8 +127,9 @@ function getColumnDefinition(fieldName, field) {
|
|
|
129
127
|
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
130
128
|
colDef = `ENUM(${enumValues})`;
|
|
131
129
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
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})` : ""}`;
|
|
134
133
|
}
|
|
135
134
|
if (field.required) colDef += ' NOT NULL';
|
|
136
135
|
const defaultDefinition = formatDefaultSql(field.default, fieldType);
|
|
@@ -158,7 +157,7 @@ class Model {
|
|
|
158
157
|
constructor(name, schema) {
|
|
159
158
|
this.name = name;
|
|
160
159
|
this.schema = schema;
|
|
161
|
-
|
|
160
|
+
|
|
162
161
|
Model.pendingModels.push(this);
|
|
163
162
|
}
|
|
164
163
|
|
|
@@ -166,7 +165,7 @@ class Model {
|
|
|
166
165
|
* Synchronizes all tables with their JS schemas (creation + adding missing columns).
|
|
167
166
|
* @returns {Promise<void>}
|
|
168
167
|
*/
|
|
169
|
-
static async syncAllTables(
|
|
168
|
+
static async syncAllTables() {
|
|
170
169
|
// Dépendances : {table: [tables dont elle dépend]}
|
|
171
170
|
const dependencies = {};
|
|
172
171
|
const modelMap = {};
|
|
@@ -181,284 +180,38 @@ class Model {
|
|
|
181
180
|
}
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
// Récupère toutes les tables existantes dans la base
|
|
185
183
|
const conn = getConnexion();
|
|
186
184
|
const [dbTablesRows] = await conn.promise().query("SHOW TABLES");
|
|
187
185
|
const dbTables = dbTablesRows.map(row => Object.values(row)[0]);
|
|
188
186
|
|
|
189
|
-
// Tri topologique
|
|
190
187
|
const sorted = [];
|
|
191
188
|
const visited = {};
|
|
192
189
|
function visit(table) {
|
|
193
|
-
if (visited
|
|
194
|
-
if (visited
|
|
195
|
-
visited
|
|
196
|
-
|
|
197
|
-
|
|
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);
|
|
198
196
|
}
|
|
199
|
-
visited
|
|
197
|
+
setSafe(visited, table, true);
|
|
200
198
|
sorted.push(table);
|
|
201
199
|
}
|
|
202
200
|
for (const table of Object.keys(dependencies)) {
|
|
203
|
-
if (!visited
|
|
201
|
+
if (!getSafe(visited, table)) visit(table);
|
|
204
202
|
}
|
|
205
203
|
|
|
206
|
-
// --- Détruit les tables qui n'ont plus de schéma ---
|
|
207
|
-
for (const dbTable of dbTables) {
|
|
208
|
-
if (!modelMap[dbTable]) {
|
|
209
|
-
try {
|
|
210
|
-
const backupPath = `./backup_${dbTable}_${Date.now()}.sql`;
|
|
211
|
-
// Sauvegarde rapide en SQL (INSERTs)
|
|
212
|
-
const [rows] = await conn.promise().query(`SELECT * FROM \`${dbTable}\``);
|
|
213
|
-
if (rows.length > 0) {
|
|
214
|
-
const columns = Object.keys(rows[0]).map(col => `\`${col}\``).join(', ');
|
|
215
|
-
const values = rows.map(row =>
|
|
216
|
-
'(' + Object.values(row).map(val =>
|
|
217
|
-
val === null ? 'NULL' : conn.escape(val)
|
|
218
|
-
).join(', ') + ')'
|
|
219
|
-
).join(',\n');
|
|
220
|
-
const insertSQL = `INSERT INTO \`${dbTable}\` (${columns}) VALUES${values};\n`;
|
|
221
|
-
fs.writeFileSync(backupPath, insertSQL, 'utf-8');
|
|
222
|
-
logs(`Sauvegarde SQL de la table '${dbTable}' effectuée dans '${backupPath}'.`);
|
|
223
|
-
} else {
|
|
224
|
-
logs(`Table '${dbTable}' vide, fichier '${backupPath}' créé.`);
|
|
225
|
-
}
|
|
226
|
-
} catch (err) {
|
|
227
|
-
error(`Erreur lors de la sauvegarde SQL de la table '${dbTable}': ${err}`);
|
|
228
|
-
}
|
|
229
|
-
logs(`Table '${dbTable}' n'a plus de schéma associé, suppression...`);
|
|
230
|
-
await conn.promise().query(`DROP TABLE IF EXISTS \`${dbTable}\``);
|
|
231
|
-
logs(`Table '${dbTable}' supprimée.`);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Création/synchronisation des tables dans l'ordre
|
|
236
204
|
for (const table of sorted) {
|
|
237
|
-
const model = modelMap
|
|
205
|
+
const model = getSafe(modelMap, table);
|
|
238
206
|
|
|
239
|
-
// 1. Crée la table si elle n'existe pas (version avec promesses)
|
|
240
207
|
try {
|
|
241
208
|
await conn.promise().query(model.generateCreateTableStatement(model.schema.schemaDict));
|
|
242
|
-
await logs(`
|
|
209
|
+
await logs(`The table ${model.name} has been created or already exists`);
|
|
243
210
|
} catch (err) {
|
|
244
211
|
error(`Error creating table: ${err} with table name: ${model.name}`);
|
|
245
212
|
throw err;
|
|
246
213
|
}
|
|
247
|
-
|
|
248
|
-
if (dangerousSync) {
|
|
249
|
-
// --- Restauration automatique si backup SQL trouvé ---
|
|
250
|
-
const backupPattern = `./backup_${model.name}_*.sql`;
|
|
251
|
-
const backupFiles = glob.sync(backupPattern).filter(f => !f.endsWith('.ignored'));
|
|
252
|
-
if (backupFiles.length > 0) {
|
|
253
|
-
const latestBackup = backupFiles.sort().reverse()[0];
|
|
254
|
-
const readline = require('readline');
|
|
255
|
-
const rl = readline.createInterface({
|
|
256
|
-
input: process.stdin,
|
|
257
|
-
output: process.stdout
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Demande restauration
|
|
261
|
-
const answer = await new Promise((resolve) => {
|
|
262
|
-
rl.question(
|
|
263
|
-
`Un backup a été trouvé pour la table '${model.name}' (${latestBackup}). Voulez-vous restaurer les données ? (y/N) `,
|
|
264
|
-
(answer) => {
|
|
265
|
-
resolve(answer.trim().toLowerCase());
|
|
266
|
-
}
|
|
267
|
-
);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
if (answer === 'y') {
|
|
271
|
-
try {
|
|
272
|
-
const sqlContent = fs.readFileSync(latestBackup, 'utf-8');
|
|
273
|
-
await conn.promise().query(sqlContent);
|
|
274
|
-
await logs(`Backup restauré pour la table '${model.name}'.`);
|
|
275
|
-
} catch (err) {
|
|
276
|
-
error(`Erreur lors de la restauration du backup pour '${model.name}': ${err}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Toujours demander la suppression après restauration ou non
|
|
281
|
-
await new Promise((resolve) => {
|
|
282
|
-
rl.question(
|
|
283
|
-
`Voulez-vous supprimer le fichier de backup '${latestBackup}' ? (y/N) `,
|
|
284
|
-
(delAnswer) => {
|
|
285
|
-
if (delAnswer.trim().toLowerCase() === 'y') {
|
|
286
|
-
fs.unlinkSync(latestBackup);
|
|
287
|
-
logs(`Backup supprimé : ${latestBackup}`);
|
|
288
|
-
} else {
|
|
289
|
-
// Renomme le fichier pour ne plus proposer la restauration
|
|
290
|
-
fs.renameSync(latestBackup, latestBackup + '.ignored');
|
|
291
|
-
logs(`Backup ignoré pour la table '${model.name}'. Il ne sera plus proposé.`);
|
|
292
|
-
}
|
|
293
|
-
rl.close();
|
|
294
|
-
resolve();
|
|
295
|
-
}
|
|
296
|
-
);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// 2. Synchronise les colonnes (renommage, ajout, suppression)
|
|
301
|
-
const [columns] = await conn.promise().query(
|
|
302
|
-
`SHOW COLUMNS FROM \`${model.name}\``
|
|
303
|
-
);
|
|
304
|
-
let existingCols = columns.map(col => col.Field);
|
|
305
|
-
|
|
306
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
307
|
-
if (existingCols.includes(fieldName)) {
|
|
308
|
-
// 1. Récupère infos colonne
|
|
309
|
-
const currentCol = columns.find(col => col.Field === fieldName);
|
|
310
|
-
const [indexes] = await conn.promise().query(
|
|
311
|
-
`SHOW INDEX FROM \`${model.name}\` WHERE Column_name = ?`, [fieldName]
|
|
312
|
-
);
|
|
313
|
-
|
|
314
|
-
// 2. Vérifie les propriétés principales avec plus de précision
|
|
315
|
-
let needModify = false;
|
|
316
|
-
|
|
317
|
-
// --- Vérification du TYPE ---
|
|
318
|
-
const fieldType = getFieldType(field);
|
|
319
|
-
let expectedTypeDef;
|
|
320
|
-
|
|
321
|
-
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
322
|
-
// Pour les ENUM, on compare la définition complète
|
|
323
|
-
const enumValues = field.enum.map(v => `'${v.replace(/'/g, "''")}'`).join(", ");
|
|
324
|
-
expectedTypeDef = `enum(${enumValues})`;
|
|
325
|
-
} else {
|
|
326
|
-
// Pour les autres types
|
|
327
|
-
if (!sqlTypeMap[fieldType]) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
|
|
328
|
-
|
|
329
|
-
expectedTypeDef = sqlTypeMap[fieldType];
|
|
330
|
-
if (sqlTypeMap[fieldType] === "VARCHAR" || sqlTypeMap[fieldType] === "INT") {
|
|
331
|
-
const length = field.length > 0 ? field.length : 255;
|
|
332
|
-
expectedTypeDef += `(${length})`;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Normalisation pour la comparaison (minuscules, suppression des espaces)
|
|
337
|
-
const normalizeType = (typeStr) => {
|
|
338
|
-
return typeStr.toLowerCase().replace(/\s+/g, '').replace(/`/g, '');
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const currentTypeNormalized = normalizeType(currentCol.Type);
|
|
342
|
-
const expectedTypeNormalized = normalizeType(expectedTypeDef);
|
|
343
|
-
|
|
344
|
-
if (currentTypeNormalized !== expectedTypeNormalized) {
|
|
345
|
-
logs(`Type mismatch for ${fieldName}: DB has '${currentCol.Type}', expected '${expectedTypeDef}'`);
|
|
346
|
-
needModify = true;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// --- Vérification NULL/NOT NULL ---
|
|
350
|
-
const shouldBeNotNull = field.required === true || field.primary_key === true;
|
|
351
|
-
const isNullableInDB = currentCol.Null === "YES";
|
|
352
|
-
|
|
353
|
-
if (shouldBeNotNull && isNullableInDB) {
|
|
354
|
-
logs(`Nullability mismatch for ${fieldName}: DB allows NULL, expected NOT NULL`);
|
|
355
|
-
needModify = true;
|
|
356
|
-
}
|
|
357
|
-
if (!shouldBeNotNull && !isNullableInDB) {
|
|
358
|
-
logs(`Nullability mismatch for ${fieldName}: DB is NOT NULL, expected NULL allowed`);
|
|
359
|
-
needModify = true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// --- Vérification DEFAULT VALUE ---
|
|
363
|
-
const expectedDefault = normalizeDefaultValue(field.default, fieldType);
|
|
364
|
-
const currentDefault = normalizeDbDefaultValue(currentCol.Default);
|
|
365
|
-
|
|
366
|
-
// Gestion spéciale pour les valeurs par défaut
|
|
367
|
-
if (expectedDefault !== null && currentDefault === null) {
|
|
368
|
-
logs(`Default value mismatch for ${fieldName}: DB has NULL, expected '${expectedDefault}'`);
|
|
369
|
-
needModify = true;
|
|
370
|
-
} else if (expectedDefault === null && currentDefault !== null) {
|
|
371
|
-
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected NULL`);
|
|
372
|
-
needModify = true;
|
|
373
|
-
} else if (expectedDefault !== null && currentDefault !== null) {
|
|
374
|
-
// Comparaison stringifiée pour éviter les problèmes de type
|
|
375
|
-
if (String(expectedDefault) !== String(currentDefault)) {
|
|
376
|
-
logs(`Default value mismatch for ${fieldName}: DB has '${currentDefault}', expected '${expectedDefault}'`);
|
|
377
|
-
needModify = true;
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// --- Vérification UNIQUE constraint ---
|
|
382
|
-
const isUniqueInDB = indexes.some(idx => idx.Non_unique === 0 && idx.Key_name !== 'PRIMARY');
|
|
383
|
-
if (!!field.unique !== isUniqueInDB) {
|
|
384
|
-
logs(`Unique constraint mismatch for ${fieldName}: DB has unique=${isUniqueInDB}, expected=${!!field.unique}`);
|
|
385
|
-
needModify = true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// --- Vérification PRIMARY KEY ---
|
|
389
|
-
const isPrimaryInDB = indexes.some(idx => idx.Key_name === 'PRIMARY');
|
|
390
|
-
if (!!field.primary_key !== isPrimaryInDB) {
|
|
391
|
-
logs(`Primary key mismatch for ${fieldName}: DB has PK=${isPrimaryInDB}, expected=${!!field.primary_key}`);
|
|
392
|
-
needModify = true;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// --- Vérification AUTO_INCREMENT ---
|
|
396
|
-
const isAutoIncrementInDB = currentCol.Extra.toLowerCase().includes('auto_increment');
|
|
397
|
-
if (!!field.auto_increment !== isAutoIncrementInDB) {
|
|
398
|
-
logs(`Auto increment mismatch for ${fieldName}: DB has AI=${isAutoIncrementInDB}, expected=${!!field.auto_increment}`);
|
|
399
|
-
needModify = true;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// 3. Si différence, on modifie
|
|
403
|
-
if (needModify) {
|
|
404
|
-
try {
|
|
405
|
-
const newColDef = getColumnDefinition(fieldName, field);
|
|
406
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` MODIFY COLUMN ${newColDef}`;
|
|
407
|
-
logs(`Modifying column ${fieldName}: ${alterSQL}`);
|
|
408
|
-
await conn.promise().query(alterSQL);
|
|
409
|
-
logs(`Column ${fieldName} modified successfully`);
|
|
410
|
-
} catch (err) {
|
|
411
|
-
error(`Error modifying column ${fieldName}: ${err}`);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// --- Warn si oldName est présent dans le schéma ---
|
|
416
|
-
if (field.oldName) {
|
|
417
|
-
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.`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// --- Étape 1 : Renommage des colonnes ---
|
|
422
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
423
|
-
if (field.oldName && existingCols.includes(field.oldName) && !existingCols.includes(fieldName)) {
|
|
424
|
-
// Génère la définition SQL de la nouvelle colonne
|
|
425
|
-
let colDef = getColumnDefinition(fieldName, field);
|
|
426
|
-
|
|
427
|
-
// Renomme la colonne
|
|
428
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` CHANGE COLUMN \`${field.oldName}\` ${colDef}`;
|
|
429
|
-
await conn.promise().query(alterSQL);
|
|
430
|
-
logs(`Colonne ${field.oldName} renommée en ${fieldName} dans ${model.name}`);
|
|
431
|
-
// Mets à jour existingCols pour la suite
|
|
432
|
-
existingCols = existingCols.map(col => col === field.oldName ? fieldName : col);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// --- Étape 2 : Ajout des colonnes manquantes ---
|
|
437
|
-
for (const [fieldName, field] of Object.entries(model.schema.schemaDict)) {
|
|
438
|
-
if (!existingCols.includes(fieldName)) {
|
|
439
|
-
let colDef = getColumnDefinition(fieldName, field);
|
|
440
|
-
|
|
441
|
-
// Ajoute la colonne
|
|
442
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` ADD COLUMN ${colDef}`;
|
|
443
|
-
await conn.promise().query(alterSQL);
|
|
444
|
-
logs(`Colonne ${fieldName} ajoutée à ${model.name}`);
|
|
445
|
-
existingCols.push(fieldName);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// --- Étape 3 : Suppression des colonnes orphelines (dangerousSync) ---
|
|
450
|
-
if (dangerousSync) {
|
|
451
|
-
for (const col of existingCols) {
|
|
452
|
-
if (!Object.keys(model.schema.schemaDict).includes(col)) {
|
|
453
|
-
const alterSQL = `ALTER TABLE \`${model.name}\` DROP COLUMN \`${col}\``;
|
|
454
|
-
await conn.promise().query(alterSQL);
|
|
455
|
-
logs(`Colonne ${col} supprimée de ${model.name}`);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
214
|
}
|
|
461
|
-
// Vide la liste d'attente
|
|
462
215
|
Model.pendingModels = [];
|
|
463
216
|
}
|
|
464
217
|
|
|
@@ -471,7 +224,7 @@ class Model {
|
|
|
471
224
|
generateCreateTableStatement(schema) {
|
|
472
225
|
let foreignKey = [];
|
|
473
226
|
const columns = Object.keys(schema).map(fieldName => {
|
|
474
|
-
|
|
227
|
+
setSafe(field, schema, fieldName);
|
|
475
228
|
let lengthDefault = 255;
|
|
476
229
|
|
|
477
230
|
if (!field.type && typeof field == "object" && !(Array.isArray(field.enum) && field.enum.length > 0)) throw new Error(`Field ${fieldName} has no type defined.`);
|
|
@@ -479,7 +232,6 @@ class Model {
|
|
|
479
232
|
const fieldType = getFieldType(field);
|
|
480
233
|
|
|
481
234
|
if (field.type && typeof field == "object") {
|
|
482
|
-
// Si c'est un enum, ne pas vérifier sqlTypeMap
|
|
483
235
|
return getColumnDefinition(fieldName, field);
|
|
484
236
|
}
|
|
485
237
|
if (Array.isArray(field.enum) && field.enum.length > 0) {
|
|
@@ -487,9 +239,11 @@ class Model {
|
|
|
487
239
|
return `${fieldName} ENUM(${enumValues})`;
|
|
488
240
|
}
|
|
489
241
|
|
|
490
|
-
|
|
242
|
+
const type = getSafe(sqlTypeMap, fieldType);
|
|
243
|
+
|
|
244
|
+
if (!type) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
|
|
491
245
|
|
|
492
|
-
return `${fieldName} ${
|
|
246
|
+
return `${fieldName} ${type == "VARCHAR" ? `${type}(${lengthDefault})` : type}`;
|
|
493
247
|
});
|
|
494
248
|
if (ifReservedKeywords(this.name)) {
|
|
495
249
|
error("Error: Invalid table name. Please choose a different name that is not a reserved keyword in SQL_request");
|
|
@@ -529,33 +283,74 @@ class Model {
|
|
|
529
283
|
}
|
|
530
284
|
}
|
|
531
285
|
|
|
286
|
+
/**
|
|
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").
|
|
292
|
+
*/
|
|
293
|
+
|
|
532
294
|
/**
|
|
533
295
|
* Retrieves multiple entries from the table.
|
|
534
296
|
* @param {Object} [options] - Query options (attributes, where, order, limit).
|
|
535
|
-
* @param {string
|
|
297
|
+
* @param {Array<string|SelectAggregation>} [options.select] - Fields to return.
|
|
536
298
|
* @param {Object} [options.where] - Filters (key/value).
|
|
537
|
-
* @param {Array} [options.order] -
|
|
538
|
-
* @param {number} [options.limit] -
|
|
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.
|
|
539
305
|
* @returns {Promise<Array<ModelInstance>>}
|
|
540
306
|
*/
|
|
541
307
|
async find(options = {}) {
|
|
542
|
-
|
|
543
|
-
|
|
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];
|
|
322
|
+
|
|
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;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
let joinClause = "";
|
|
339
|
+
if (join && join.table && join.on) {
|
|
340
|
+
joinClause = ` INNER JOIN ${join.table} ON ${join.on}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const query = `SELECT ${buildSelect(select)} FROM ${this.name}${joinClause} ${buildQueryParts(options)}`;
|
|
544
344
|
|
|
545
345
|
return new Promise(async (resolve, reject) => {
|
|
546
346
|
await getConnexion().promise().query(query).then((result) => {
|
|
547
|
-
// Le driver mysql2 renvoie [rows, fields], on isole le tableau de lignes 'rows'
|
|
548
347
|
const rows = result && Array.isArray(result) ? result[0] : result;
|
|
549
|
-
|
|
550
|
-
// S'il n'y a aucun résultat, on renvoie un tableau vide [] (et pas 0, c'est plus propre pour faire des .length)
|
|
551
348
|
if (!rows || rows.length === 0) return resolve([]);
|
|
552
349
|
|
|
553
|
-
// 🚀 LE DÉCALAGE : On transforme chaque ligne brute en une ModelInstance unique
|
|
554
350
|
const instances = rows.map(row => new ModelInstance(this.name, row, this.schema));
|
|
555
|
-
|
|
556
351
|
resolve(instances);
|
|
557
352
|
}).catch((err) => {
|
|
558
|
-
error(`Error executing
|
|
353
|
+
error(`Error executing auto-prefixed find: ${err}`);
|
|
559
354
|
reject(err);
|
|
560
355
|
});
|
|
561
356
|
});
|
|
@@ -4,6 +4,7 @@ const formatObject = require("../utils/formatObject");
|
|
|
4
4
|
const generateCondition = require("../utils/generateCondition");
|
|
5
5
|
const util = require("util");
|
|
6
6
|
const { serveur } = require("@mlagie/logger");
|
|
7
|
+
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Represents an instance of a database model.
|
|
@@ -40,21 +41,18 @@ class ModelInstance {
|
|
|
40
41
|
const row = this._getTargetRow();
|
|
41
42
|
if (row && typeof row === 'object') {
|
|
42
43
|
Object.keys(row).forEach(key => {
|
|
43
|
-
// On lie dynamiquement la clé de l'instance directement à la case mémoire de 'row'
|
|
44
44
|
Object.defineProperty(this, key, {
|
|
45
45
|
get: () => {
|
|
46
|
-
const val = row
|
|
47
|
-
// Auto-parse propre du JSON si la colonne MySQL stocke une String JSON
|
|
46
|
+
const val = getSafe(row, key);
|
|
48
47
|
if (typeof val === 'string' && val.trim().startsWith('{') && val.trim().endsWith('}')) {
|
|
49
48
|
try { return JSON.parse(val); } catch (e) { return val; }
|
|
50
49
|
}
|
|
51
50
|
return val;
|
|
52
51
|
},
|
|
53
52
|
set: (newVal) => {
|
|
54
|
-
|
|
55
|
-
row[key] = newVal;
|
|
53
|
+
setSafe(row, key, newVal);
|
|
56
54
|
},
|
|
57
|
-
enumerable: true,
|
|
55
|
+
enumerable: true,
|
|
58
56
|
configurable: true
|
|
59
57
|
});
|
|
60
58
|
});
|
|
@@ -62,7 +60,7 @@ class ModelInstance {
|
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
/**
|
|
65
|
-
*
|
|
63
|
+
* Extract the actual data row by managing the database driver's structure [rows, fields]
|
|
66
64
|
* @private
|
|
67
65
|
*/
|
|
68
66
|
_getTargetRow() {
|
|
@@ -94,19 +92,14 @@ class ModelInstance {
|
|
|
94
92
|
|
|
95
93
|
let whereClause;
|
|
96
94
|
try {
|
|
97
|
-
// 1. On récupère le tableau de données
|
|
98
95
|
const recordsArray = this.getRecordData();
|
|
99
96
|
|
|
100
|
-
// 2. On extrait le premier élément (la ligne actuelle)
|
|
101
97
|
let rawRec = Array.isArray(recordsArray) ? recordsArray[0] : recordsArray;
|
|
102
98
|
|
|
103
|
-
// 3. Si cet élément est une chaîne JSON, on le transforme en vrai objet JS
|
|
104
99
|
if (typeof rawRec === 'string') {
|
|
105
100
|
try {
|
|
106
101
|
rawRec = JSON.parse(rawRec);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
// Pas du JSON valide, on garde la string d'origine
|
|
109
|
-
}
|
|
102
|
+
} catch (e) { }
|
|
110
103
|
}
|
|
111
104
|
const rec = rawRec;
|
|
112
105
|
|
|
@@ -116,14 +109,13 @@ class ModelInstance {
|
|
|
116
109
|
if (pkKeys.length > 0) {
|
|
117
110
|
const pkObj = {};
|
|
118
111
|
for (const k of pkKeys) {
|
|
119
|
-
if (rec && Object.prototype.hasOwnProperty.call(rec, k)) pkObj
|
|
112
|
+
if (rec && Object.prototype.hasOwnProperty.call(rec, k)) setSafe(pkObj, k, getSafe(rec, k));
|
|
120
113
|
}
|
|
121
114
|
if (Object.keys(pkObj).length > 0) whereClause = generateCondition(formatObject(pkObj), false, this.schema);
|
|
122
115
|
}
|
|
123
116
|
}
|
|
124
117
|
if (!whereClause) whereClause = generateCondition(formatObject(rec), false, this.schema);
|
|
125
118
|
} catch (e) {
|
|
126
|
-
// Fallback de sécurité au cas où
|
|
127
119
|
let fallbackRec = this.getRecordData();
|
|
128
120
|
if (Array.isArray(fallbackRec)) fallbackRec = fallbackRec[0];
|
|
129
121
|
if (typeof fallbackRec === 'string') { try { fallbackRec = JSON.parse(fallbackRec); } catch (e) { } }
|
|
@@ -139,7 +131,6 @@ class ModelInstance {
|
|
|
139
131
|
|
|
140
132
|
const affected = result && (result.affectedRows !== undefined ? result.affectedRows : 0);
|
|
141
133
|
|
|
142
|
-
// Update in-memory data if DB was modified
|
|
143
134
|
if (affected > 0) {
|
|
144
135
|
const record = this.getRecordData();
|
|
145
136
|
if (Array.isArray(this.data)) {
|
package/src/utils/buildQuery.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
const { getSafe, setSafe } = require("./security/safe");
|
|
2
|
+
|
|
1
3
|
module.exports = function (obj) {
|
|
2
4
|
for (const key in obj) {
|
|
3
5
|
if (obj.hasOwnProperty(key)) {
|
|
4
|
-
const value = obj
|
|
6
|
+
const value = getSafe(obj, key);
|
|
5
7
|
if (value instanceof Date) {
|
|
6
8
|
// convert Date to MySQL DATETIME (no timezone)
|
|
7
|
-
obj
|
|
9
|
+
setSafe(obj, key, value.toISOString().slice(0, 19).replace('T', ' '));
|
|
8
10
|
continue;
|
|
9
11
|
}
|
|
10
12
|
|
|
@@ -17,13 +19,13 @@ module.exports = function (obj) {
|
|
|
17
19
|
}
|
|
18
20
|
// unescape common escaped quotes
|
|
19
21
|
v = v.replace(/\\"/g, '"').replace(/\\'/g, "'");
|
|
20
|
-
obj
|
|
22
|
+
setSafe(obj, key, v);
|
|
21
23
|
continue;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
if (typeof value === "object") {
|
|
25
27
|
// stringify objects and escape single quotes for SQL safety
|
|
26
|
-
obj
|
|
28
|
+
setSafe(obj, key, JSON.stringify(value).replace(/'/g, "\\'"));
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getSafe } = require("./security/safe");
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Generates an SQL_request condition from a filter object.
|
|
3
5
|
*
|
|
@@ -19,17 +21,17 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
19
21
|
const keys = Object.keys(filter);
|
|
20
22
|
const values = Object.values(filter);
|
|
21
23
|
|
|
22
|
-
const filteredKeys = isUpdate ? keys.filter(key => filter
|
|
23
|
-
const filteredValues = isUpdate ? filteredKeys.map(key => filter
|
|
24
|
+
const filteredKeys = isUpdate ? keys.filter(key => getSafe(filter, key) !== undefined) : keys;
|
|
25
|
+
const filteredValues = isUpdate ? filteredKeys.map(key => getSafe(filter, key)) : values;
|
|
24
26
|
|
|
25
27
|
if (!isUpdate && schema && schema.schemaDict) {
|
|
26
28
|
const uniqueKeys = filteredKeys.filter(key => {
|
|
27
|
-
const field = schema.schemaDict
|
|
29
|
+
const field = getSafe(schema.schemaDict, key);
|
|
28
30
|
return field && field.unique === true;
|
|
29
31
|
});
|
|
30
32
|
if (uniqueKeys.length > 0) {
|
|
31
33
|
return uniqueKeys.map(key => {
|
|
32
|
-
let value = filter
|
|
34
|
+
let value = getSafe(filter, key);
|
|
33
35
|
// normalize strings that may contain surrounding quotes or escaped quotes
|
|
34
36
|
if (typeof value === 'string') {
|
|
35
37
|
value = value.trim();
|
|
@@ -58,7 +60,7 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
58
60
|
|
|
59
61
|
// Comportement par défaut
|
|
60
62
|
const conditions = filteredKeys.map((key, index) => {
|
|
61
|
-
let value = filteredValues
|
|
63
|
+
let value = getSafe(filteredValues, index);
|
|
62
64
|
|
|
63
65
|
if (typeof value === 'string') {
|
|
64
66
|
value = value.trim();
|
|
@@ -82,7 +84,7 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
82
84
|
if ((value === null || value === "null") && isUpdate == false) return `${key} IS NULL`;
|
|
83
85
|
|
|
84
86
|
// handle date-like strings when schema tells us the field is temporal
|
|
85
|
-
const fieldDef = schema && schema.schemaDict ? schema.schemaDict
|
|
87
|
+
const fieldDef = schema && schema.schemaDict ? getSafe(schema.schemaDict, key) : null;
|
|
86
88
|
let fieldType = null;
|
|
87
89
|
if (fieldDef) {
|
|
88
90
|
if (fieldDef.type && fieldDef.type.name !== undefined) fieldType = fieldDef.type.name;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blacklist of prohibited keys to prevent prototype pollution.
|
|
3
|
+
*/
|
|
4
|
+
const BLACKLIST = new Set(['__proto__', 'constructor', 'prototype']);
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extracts a value in a secure manner from an object.
|
|
8
|
+
* @param {Object} obj - The target object
|
|
9
|
+
* @param {string} key - The key or property to read
|
|
10
|
+
* @returns {*} The value or undefined if not found / prohibited
|
|
11
|
+
*/
|
|
12
|
+
function getSafe(obj, key) {
|
|
13
|
+
if (!obj || BLACKLIST.has(key)) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return Reflect.get(obj, key);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sets a value in a secure manner within an object.
|
|
22
|
+
* @param {Object} obj - The target object
|
|
23
|
+
* @param {string} key - The key or property to write
|
|
24
|
+
* @param {*} value - The value to assign
|
|
25
|
+
* @returns {boolean} True if the operation was successful, false otherwise
|
|
26
|
+
*/
|
|
27
|
+
function setSafe(obj, key, value) {
|
|
28
|
+
if (!obj || BLACKLIST.has(key)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return Reflect.set(obj, key, value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { getSafe, setSafe };
|