@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,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 rec = this.getRecordData();
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[k] = rec[k];
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
- whereClause = generateCondition(formatObject(this.getRecordData()), false, this.schema);
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[0], this.schema));
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[key];
6
+ const value = getSafe(obj, key);
5
7
  if (value instanceof Date) {
6
8
  // convert Date to MySQL DATETIME (no timezone)
7
- obj[key] = value.toISOString().slice(0, 19).replace('T', ' ');
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[key] = v;
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[key] = JSON.stringify(value).replace(/'/g, "\\'");
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[key] !== undefined) : keys;
23
- const filteredValues = isUpdate ? filteredKeys.map(key => filter[key]) : values;
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[key];
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[key];
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[index];
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[key] : null;
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 };