@mlagie/sql-connector 1.4.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +43 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +29 -0
- package/.github/workflows/publish.yml +47 -0
- package/README.md +260 -48
- package/docs/fr/README.md +204 -24
- package/eslint.config.mjs +12 -0
- package/index.d.ts +15 -45
- package/package.json +7 -3
- package/releases/1.5.0.md +127 -0
- package/releases/2.0.0.md +32 -0
- package/src/models/Model.js +110 -368
- package/src/models/ModelInstance.js +52 -10
- package/src/utils/buildQuery.js +72 -0
- package/src/utils/formatObject.js +6 -4
- package/src/utils/generateCondition.js +9 -6
- package/src/utils/security/safe.js +35 -0
|
@@ -3,6 +3,8 @@ 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
|
+
const { getSafe, setSafe } = require("../utils/security/safe");
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Represents an instance of a database model.
|
|
@@ -36,6 +38,34 @@ class ModelInstance {
|
|
|
36
38
|
enumerable: false
|
|
37
39
|
}
|
|
38
40
|
});
|
|
41
|
+
const row = this._getTargetRow();
|
|
42
|
+
if (row && typeof row === 'object') {
|
|
43
|
+
Object.keys(row).forEach(key => {
|
|
44
|
+
Object.defineProperty(this, key, {
|
|
45
|
+
get: () => {
|
|
46
|
+
const val = getSafe(row, key);
|
|
47
|
+
if (typeof val === 'string' && val.trim().startsWith('{') && val.trim().endsWith('}')) {
|
|
48
|
+
try { return JSON.parse(val); } catch (e) { return val; }
|
|
49
|
+
}
|
|
50
|
+
return val;
|
|
51
|
+
},
|
|
52
|
+
set: (newVal) => {
|
|
53
|
+
setSafe(row, key, newVal);
|
|
54
|
+
},
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract the actual data row by managing the database driver's structure [rows, fields]
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
_getTargetRow() {
|
|
67
|
+
const rows = Array.isArray(this.data) && Array.isArray(this.data[0]) ? this.data[0] : this.data;
|
|
68
|
+
return Array.isArray(rows) ? rows[0] : rows;
|
|
39
69
|
}
|
|
40
70
|
|
|
41
71
|
getRecordData() {
|
|
@@ -60,35 +90,47 @@ class ModelInstance {
|
|
|
60
90
|
async updateOne(model) {
|
|
61
91
|
const setClause = generateCondition(formatObject(model), true);
|
|
62
92
|
|
|
63
|
-
// prefer primary key(s) in WHERE to avoid mismatches on nullable/text fields
|
|
64
93
|
let whereClause;
|
|
65
94
|
try {
|
|
66
|
-
const
|
|
95
|
+
const recordsArray = this.getRecordData();
|
|
96
|
+
|
|
97
|
+
let rawRec = Array.isArray(recordsArray) ? recordsArray[0] : recordsArray;
|
|
98
|
+
|
|
99
|
+
if (typeof rawRec === 'string') {
|
|
100
|
+
try {
|
|
101
|
+
rawRec = JSON.parse(rawRec);
|
|
102
|
+
} catch (e) { }
|
|
103
|
+
}
|
|
104
|
+
const rec = rawRec;
|
|
105
|
+
|
|
67
106
|
const schemaDict = this.schema && this.schema.schemaDict ? this.schema.schemaDict : null;
|
|
68
107
|
if (schemaDict) {
|
|
69
108
|
const pkKeys = Object.entries(schemaDict).filter(([k, v]) => v && v.primary_key === true).map(([k]) => k);
|
|
70
109
|
if (pkKeys.length > 0) {
|
|
71
110
|
const pkObj = {};
|
|
72
111
|
for (const k of pkKeys) {
|
|
73
|
-
if (rec && Object.prototype.hasOwnProperty.call(rec, k)) pkObj
|
|
112
|
+
if (rec && Object.prototype.hasOwnProperty.call(rec, k)) setSafe(pkObj, k, getSafe(rec, k));
|
|
74
113
|
}
|
|
75
114
|
if (Object.keys(pkObj).length > 0) whereClause = generateCondition(formatObject(pkObj), false, this.schema);
|
|
76
115
|
}
|
|
77
116
|
}
|
|
78
117
|
if (!whereClause) whereClause = generateCondition(formatObject(rec), false, this.schema);
|
|
79
118
|
} catch (e) {
|
|
80
|
-
|
|
119
|
+
let fallbackRec = this.getRecordData();
|
|
120
|
+
if (Array.isArray(fallbackRec)) fallbackRec = fallbackRec[0];
|
|
121
|
+
if (typeof fallbackRec === 'string') { try { fallbackRec = JSON.parse(fallbackRec); } catch (e) { } }
|
|
122
|
+
whereClause = generateCondition(formatObject(fallbackRec), false, this.schema);
|
|
81
123
|
}
|
|
124
|
+
|
|
82
125
|
const sql_request = `UPDATE ${this.name} SET ${setClause} WHERE ${whereClause}`;
|
|
83
126
|
|
|
84
127
|
const [result] = await getConnexion().promise().query(sql_request).catch((err) => {
|
|
85
|
-
error(`Error executing query: ${err}`);
|
|
128
|
+
error(`Error executing query updateOne: ${err}`);
|
|
86
129
|
throw err;
|
|
87
130
|
});
|
|
88
131
|
|
|
89
132
|
const affected = result && (result.affectedRows !== undefined ? result.affectedRows : 0);
|
|
90
133
|
|
|
91
|
-
// Update in-memory data if DB was modified
|
|
92
134
|
if (affected > 0) {
|
|
93
135
|
const record = this.getRecordData();
|
|
94
136
|
if (Array.isArray(this.data)) {
|
|
@@ -117,7 +159,7 @@ class ModelInstance {
|
|
|
117
159
|
|
|
118
160
|
resolve(1);
|
|
119
161
|
}).catch((err) => {
|
|
120
|
-
error(`Error executing query: ${err}`);
|
|
162
|
+
error(`Error executing query delete: ${err}`);
|
|
121
163
|
return 0;
|
|
122
164
|
});
|
|
123
165
|
});
|
|
@@ -137,7 +179,7 @@ class ModelInstance {
|
|
|
137
179
|
|
|
138
180
|
resolve(1);
|
|
139
181
|
}).catch((err) => {
|
|
140
|
-
error(`Error executing query: ${err}`);
|
|
182
|
+
error(`Error executing query deleteOne: ${err}`);
|
|
141
183
|
return 0;
|
|
142
184
|
});
|
|
143
185
|
});
|
|
@@ -154,7 +196,7 @@ class ModelInstance {
|
|
|
154
196
|
await getConnexion().promise().query(custom).then((rows) => {
|
|
155
197
|
if (rows.length == 0) return resolve(0);
|
|
156
198
|
|
|
157
|
-
resolve(new ModelInstance(this.name, rows
|
|
199
|
+
resolve(new ModelInstance(this.name, rows, this.schema)).data;
|
|
158
200
|
}).catch((err) => {
|
|
159
201
|
error(`Error executing query: ${err}`);
|
|
160
202
|
return;
|
|
@@ -163,4 +205,4 @@ class ModelInstance {
|
|
|
163
205
|
}
|
|
164
206
|
}
|
|
165
207
|
|
|
166
|
-
module.exports = {ModelInstance}
|
|
208
|
+
module.exports = { ModelInstance }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const formatObject = require("./formatObject");
|
|
2
|
+
const generateCondition = require("./generateCondition");
|
|
3
|
+
|
|
4
|
+
function buildField(field) {
|
|
5
|
+
if (typeof field === 'string') {
|
|
6
|
+
return field;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let sql = '';
|
|
10
|
+
|
|
11
|
+
if (field.sum)
|
|
12
|
+
sql = `SUM(${field.sum})`;
|
|
13
|
+
else if (field.dateFormat) {
|
|
14
|
+
const [col, format] = field.dateFormat;
|
|
15
|
+
sql = `DATE_FORMAT(${col}, '${format}')`;
|
|
16
|
+
}
|
|
17
|
+
else if (field.col)
|
|
18
|
+
sql = field.col;
|
|
19
|
+
|
|
20
|
+
if (field.as)
|
|
21
|
+
sql += ` AS ${field.as}`;
|
|
22
|
+
else if (field.sum)
|
|
23
|
+
sql += ` AS ${field.sum}`;
|
|
24
|
+
return sql;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildSelect(select = []) {
|
|
28
|
+
if (!select || select.length === 0) {
|
|
29
|
+
return '*';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return select
|
|
33
|
+
.map(field => buildField(field))
|
|
34
|
+
.join(',\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function buildQueryParts(options) {
|
|
38
|
+
const parts = [];
|
|
39
|
+
|
|
40
|
+
if (options.where) {
|
|
41
|
+
if (typeof options.where === 'string') {
|
|
42
|
+
parts.push(`WHERE ${options.where}`);
|
|
43
|
+
} else {
|
|
44
|
+
parts.push(`WHERE ${generateCondition(formatObject(options.where))}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.groupBy) {
|
|
49
|
+
parts.push(`GROUP BY ${options.groupBy.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (options.having) {
|
|
53
|
+
parts.push(`HAVING ${options.having}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.orderBy) {
|
|
57
|
+
const order = options.orderBy.map(o =>
|
|
58
|
+
typeof o === 'string'
|
|
59
|
+
? o
|
|
60
|
+
: `${o.field} ${o.direction || 'ASC'}`
|
|
61
|
+
);
|
|
62
|
+
parts.push(`ORDER BY ${order.join(', ')}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (options.limit) {
|
|
66
|
+
parts.push(`LIMIT ${options.limit}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return parts.join('\n\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { buildQueryParts, buildSelect }
|
|
@@ -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,8 @@ 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);
|
|
64
|
+
|
|
62
65
|
if (typeof value === 'string') {
|
|
63
66
|
value = value.trim();
|
|
64
67
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
@@ -81,7 +84,7 @@ module.exports = function (filter, isUpdate = false, schema = null) {
|
|
|
81
84
|
if ((value === null || value === "null") && isUpdate == false) return `${key} IS NULL`;
|
|
82
85
|
|
|
83
86
|
// handle date-like strings when schema tells us the field is temporal
|
|
84
|
-
const fieldDef = schema && schema.schemaDict ? schema.schemaDict
|
|
87
|
+
const fieldDef = schema && schema.schemaDict ? getSafe(schema.schemaDict, key) : null;
|
|
85
88
|
let fieldType = null;
|
|
86
89
|
if (fieldDef) {
|
|
87
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 };
|