masterrecord 0.3.16 → 0.3.17
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/Migrations/schema.js +10 -9
- package/SQLLiteEngine.js +72 -48
- package/context.js +48 -38
- package/deleteManager.js +11 -11
- package/insertManager.js +7 -7
- package/mySQLAsyncConnect.js +44 -0
- package/package.json +2 -2
- package/realMySQLEngine.js +836 -0
- package/test/parameterizedPlaceholderTest.js +1 -1
- package/mySQLEngine.js +0 -1105
- package/mySQLSyncConnect.js +0 -82
package/mySQLEngine.js
DELETED
|
@@ -1,1105 +0,0 @@
|
|
|
1
|
-
// version : 0.0.9
|
|
2
|
-
|
|
3
|
-
var tools = require('masterrecord/Tools');
|
|
4
|
-
var util = require('util');
|
|
5
|
-
var FieldTransformer = require('masterrecord/Entity/fieldTransformer');
|
|
6
|
-
|
|
7
|
-
class SQLLiteEngine {
|
|
8
|
-
|
|
9
|
-
unsupportedWords = ["order"]
|
|
10
|
-
|
|
11
|
-
update(query){
|
|
12
|
-
// Security: ONLY use parameterized queries - no fallback to string concatenation
|
|
13
|
-
// query.arg must contain {sql, params} from _buildSQLEqualToParameterized
|
|
14
|
-
if(!query.arg || typeof query.arg !== 'object' || !query.arg.sql || !query.arg.params){
|
|
15
|
-
throw new Error('UPDATE failed: Invalid parameterized query structure. Check entity definition.');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
var sqlQuery = ` UPDATE ${query.tableName} SET ${query.arg.sql} WHERE ${query.tableName}.${query.primaryKey} = ?`;
|
|
19
|
-
// Add primaryKeyValue to params array
|
|
20
|
-
var params = [...query.arg.params, query.primaryKeyValue];
|
|
21
|
-
return this._runWithParams(sqlQuery, params);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
delete(queryObject){
|
|
25
|
-
var sqlObject = this._buildDeleteObject(queryObject);
|
|
26
|
-
// Use parameterized query to prevent SQL injection
|
|
27
|
-
var sqlQuery = `DELETE FROM ${sqlObject.tableName} WHERE ${sqlObject.tableName}.${sqlObject.primaryKey} = ?`;
|
|
28
|
-
return this._runWithParams(sqlQuery, [sqlObject.value]);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
insert(queryObject){
|
|
32
|
-
// Use NEW SECURE parameterized version
|
|
33
|
-
var sqlObject = this._buildSQLInsertObjectParameterized(queryObject, queryObject.__entity);
|
|
34
|
-
if(sqlObject === -1){
|
|
35
|
-
throw new Error('INSERT failed: No columns to insert');
|
|
36
|
-
}
|
|
37
|
-
var query = `INSERT INTO ${sqlObject.tableName} (${sqlObject.columns}) VALUES (${sqlObject.placeholders})`;
|
|
38
|
-
var queryObj = this._runWithParams(query, sqlObject.params);
|
|
39
|
-
// return
|
|
40
|
-
var open = {
|
|
41
|
-
"id": queryObj.insertId
|
|
42
|
-
};
|
|
43
|
-
return open;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Batch insert using MySQL's multi-value INSERT
|
|
48
|
-
* INSERT INTO table (col1, col2) VALUES (?, ?), (?, ?), (?, ?)
|
|
49
|
-
*/
|
|
50
|
-
bulkInsert(entities) {
|
|
51
|
-
if (!entities || entities.length === 0) return [];
|
|
52
|
-
|
|
53
|
-
// Group by table name
|
|
54
|
-
const byTable = {};
|
|
55
|
-
for (const entity of entities) {
|
|
56
|
-
const tableName = entity.__entity.__name;
|
|
57
|
-
if (!byTable[tableName]) byTable[tableName] = [];
|
|
58
|
-
byTable[tableName].push(entity);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const results = [];
|
|
62
|
-
for (const tableName in byTable) {
|
|
63
|
-
const tableEntities = byTable[tableName];
|
|
64
|
-
|
|
65
|
-
// Build multi-value INSERT
|
|
66
|
-
const first = this._buildSQLInsertObjectParameterized(tableEntities[0], tableEntities[0].__entity);
|
|
67
|
-
const allParams = [...first.params];
|
|
68
|
-
const valueGroups = [`(${first.placeholders})`];
|
|
69
|
-
|
|
70
|
-
for (let i = 1; i < tableEntities.length; i++) {
|
|
71
|
-
const sqlObj = this._buildSQLInsertObjectParameterized(tableEntities[i], tableEntities[i].__entity);
|
|
72
|
-
valueGroups.push(`(${sqlObj.placeholders})`);
|
|
73
|
-
allParams.push(...sqlObj.params);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const query = `INSERT INTO \`${first.tableName}\` (${first.columns}) VALUES ${valueGroups.join(', ')}`;
|
|
77
|
-
const result = this._runWithParams(query, allParams);
|
|
78
|
-
results.push(result);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return results;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Batch update (execute in sequence for MySQL)
|
|
86
|
-
*/
|
|
87
|
-
bulkUpdate(updateQueries) {
|
|
88
|
-
if (!updateQueries || updateQueries.length === 0) return;
|
|
89
|
-
|
|
90
|
-
for (const query of updateQueries) {
|
|
91
|
-
this.update(query);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Batch delete using WHERE IN
|
|
97
|
-
*/
|
|
98
|
-
bulkDelete(tableName, ids) {
|
|
99
|
-
if (!ids || ids.length === 0) return;
|
|
100
|
-
|
|
101
|
-
const placeholders = ids.map(() => '?').join(', ');
|
|
102
|
-
const query = `DELETE FROM \`${tableName}\` WHERE id IN (${placeholders})`;
|
|
103
|
-
return this._runWithParams(query, ids);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
get(query, entity, context){
|
|
107
|
-
var queryString = {};
|
|
108
|
-
try {
|
|
109
|
-
if(query.raw){
|
|
110
|
-
queryString.query = query.raw;
|
|
111
|
-
}
|
|
112
|
-
else{
|
|
113
|
-
queryString = this.buildQuery(query, entity, context);
|
|
114
|
-
}
|
|
115
|
-
if(queryString.query){
|
|
116
|
-
// Get parameters from query script
|
|
117
|
-
const params = query.parameters ? query.parameters.getParams() : [];
|
|
118
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
119
|
-
console.debug("[SQL]", queryString.query);
|
|
120
|
-
console.debug("[Params]", params);
|
|
121
|
-
}
|
|
122
|
-
this.db.connect(this.db);
|
|
123
|
-
const result = this.db.query(queryString.query, params);
|
|
124
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
125
|
-
console.debug("results:", result);
|
|
126
|
-
}
|
|
127
|
-
return result;
|
|
128
|
-
}
|
|
129
|
-
return null;
|
|
130
|
-
} catch (err) {
|
|
131
|
-
console.error(err);
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
getCount(queryObject, entity, context){
|
|
137
|
-
var query = queryObject.script;
|
|
138
|
-
var queryString = {};
|
|
139
|
-
try {
|
|
140
|
-
if(query.raw){
|
|
141
|
-
queryString.query = query.raw;
|
|
142
|
-
}
|
|
143
|
-
else{
|
|
144
|
-
queryString = this.buildQuery(query, entity, context);
|
|
145
|
-
}
|
|
146
|
-
if(queryString.query){
|
|
147
|
-
var queryCount = queryObject.count(queryString.query)
|
|
148
|
-
// Get parameters from query script
|
|
149
|
-
const params = query.parameters ? query.parameters.getParams() : [];
|
|
150
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
151
|
-
console.debug("[SQL]", queryCount);
|
|
152
|
-
console.debug("[Params]", params);
|
|
153
|
-
}
|
|
154
|
-
this.db.connect(this.db);
|
|
155
|
-
var queryReturn = this.db.query(queryCount, params);
|
|
156
|
-
return queryReturn[0]; // MySQL returns array, get first row
|
|
157
|
-
}
|
|
158
|
-
return null;
|
|
159
|
-
} catch (err) {
|
|
160
|
-
console.error(err);
|
|
161
|
-
return null;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
all(query, entity, context){
|
|
166
|
-
var queryString = {};
|
|
167
|
-
try {
|
|
168
|
-
if(query.raw){
|
|
169
|
-
queryString.query = query.raw;
|
|
170
|
-
}
|
|
171
|
-
else{
|
|
172
|
-
queryString = this.buildQuery(query, entity, context);
|
|
173
|
-
}
|
|
174
|
-
if(queryString.query){
|
|
175
|
-
// Get parameters from query script
|
|
176
|
-
const params = query.parameters ? query.parameters.getParams() : [];
|
|
177
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
178
|
-
console.debug("[SQL]", queryString.query);
|
|
179
|
-
console.debug("[Params]", params);
|
|
180
|
-
}
|
|
181
|
-
this.db.connect(this.db);
|
|
182
|
-
const result = this.db.query(queryString.query, params);
|
|
183
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
184
|
-
console.debug("results:", result);
|
|
185
|
-
}
|
|
186
|
-
return result;
|
|
187
|
-
}
|
|
188
|
-
return null;
|
|
189
|
-
} catch (err) {
|
|
190
|
-
console.error(err);
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Introspection helpers
|
|
196
|
-
tableExists(tableName){
|
|
197
|
-
try{
|
|
198
|
-
const sql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`;
|
|
199
|
-
this.db.connect(this.db);
|
|
200
|
-
const res = this.db.query(sql, [tableName]);
|
|
201
|
-
return Array.isArray(res) ? res.length > 0 : !!res?.length;
|
|
202
|
-
}catch(_){ return false; }
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
getTableInfo(tableName){
|
|
206
|
-
try{
|
|
207
|
-
const sql = `SELECT COLUMN_NAME as name, COLUMN_DEFAULT as dflt_value, IS_NULLABLE as is_nullable, DATA_TYPE as data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`;
|
|
208
|
-
this.db.connect(this.db);
|
|
209
|
-
const res = this.db.query(sql, [tableName]);
|
|
210
|
-
return res || [];
|
|
211
|
-
}catch(_){ return []; }
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
buildQuery(query, entity, context){
|
|
216
|
-
|
|
217
|
-
var queryObject = {};
|
|
218
|
-
if(entity){
|
|
219
|
-
queryObject.entity = this.getEntity(entity.__name, query.entityMap);
|
|
220
|
-
queryObject.select = this.buildSelect(query, entity);
|
|
221
|
-
queryObject.from = this.buildFrom(query, entity);
|
|
222
|
-
queryObject.include = this.buildInclude(query, entity, context, queryObject);
|
|
223
|
-
queryObject.where = this.buildWhere(query, entity);
|
|
224
|
-
|
|
225
|
-
var queryString = `${queryObject.select} ${queryObject.from} ${queryObject.include} ${queryObject.where}`;
|
|
226
|
-
return {
|
|
227
|
-
query : queryString,
|
|
228
|
-
entity : this.getEntity(entity.__name, query.entityMap)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
else{
|
|
232
|
-
console.log("Error: Entity object is blank");
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
buildWhere(query, mainQuery){
|
|
239
|
-
var whereEntity = query.where;
|
|
240
|
-
var $that = this;
|
|
241
|
-
if(!whereEntity){
|
|
242
|
-
return "";
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
var entityAlias = this.getEntity(query.parentName, query.entityMap);
|
|
246
|
-
var item = whereEntity[query.parentName].query;
|
|
247
|
-
var exprs = item.expressions || [];
|
|
248
|
-
|
|
249
|
-
function exprToSql(expr){
|
|
250
|
-
var field = expr.field.toLowerCase();
|
|
251
|
-
var ent = entityAlias;
|
|
252
|
-
if(mainQuery[field]){
|
|
253
|
-
if(mainQuery[field].isNavigational){
|
|
254
|
-
ent = $that.getEntity(field, query.entityMap);
|
|
255
|
-
if(item.fields && item.fields[1]){
|
|
256
|
-
field = item.fields[1];
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
let func = expr.func;
|
|
261
|
-
let arg = expr.arg;
|
|
262
|
-
if((!func && typeof arg === 'undefined')){
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
// Removed fallback that coerced 'exists' with an argument to '='
|
|
266
|
-
// Bare field or !field: interpret as IS [NOT] NULL
|
|
267
|
-
if(func === 'exists' && typeof arg === 'undefined'){
|
|
268
|
-
const isNull = expr.negate === true; // '!field' -> IS NULL
|
|
269
|
-
return `${ent}.${field} is ${isNull ? '' : 'not '}null`;
|
|
270
|
-
}
|
|
271
|
-
if(arg === "null"){
|
|
272
|
-
if(func === "=") func = "is";
|
|
273
|
-
if(func === "!=") func = "is not";
|
|
274
|
-
return `${ent}.${field} ${func} ${arg}`;
|
|
275
|
-
}
|
|
276
|
-
if(func === "IN"){
|
|
277
|
-
return `${ent}.${field} ${func} ${arg}`;
|
|
278
|
-
}
|
|
279
|
-
// Check if arg is a parameterized placeholder (? for MySQL/SQLite, $1/$2/etc for Postgres)
|
|
280
|
-
var isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
281
|
-
if(isPlaceholder){
|
|
282
|
-
// Don't quote placeholders - they must remain as bare ? or $1
|
|
283
|
-
return `${ent}.${field} ${func} ${arg}`;
|
|
284
|
-
}
|
|
285
|
-
var safeArg = (typeof arg === 'string' || arg instanceof String)
|
|
286
|
-
? $that._santizeSingleQuotes(arg, { entityName: ent, fieldName: field })
|
|
287
|
-
: String(arg);
|
|
288
|
-
return `${ent}.${field} ${func} '${safeArg}'`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const pieces = [];
|
|
292
|
-
for(let i = 0; i < exprs.length; i++){
|
|
293
|
-
const e = exprs[i];
|
|
294
|
-
if(e.group){
|
|
295
|
-
const gid = e.group;
|
|
296
|
-
const orParts = [];
|
|
297
|
-
while(i < exprs.length && exprs[i].group === gid){
|
|
298
|
-
const sql = exprToSql(exprs[i]);
|
|
299
|
-
if(sql){ orParts.push(sql); }
|
|
300
|
-
i++;
|
|
301
|
-
}
|
|
302
|
-
i--; // compensate for loop increment
|
|
303
|
-
if(orParts.length > 0){
|
|
304
|
-
pieces.push(`(${orParts.join(" or ")})`);
|
|
305
|
-
}
|
|
306
|
-
}else{
|
|
307
|
-
const sql = exprToSql(e);
|
|
308
|
-
if(sql){ pieces.push(sql); }
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if(pieces.length === 0){
|
|
313
|
-
return "";
|
|
314
|
-
}
|
|
315
|
-
return `WHERE ${pieces.join(" and ")}`;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
buildInclude( query, entity, context){
|
|
319
|
-
const includeQueries = [];
|
|
320
|
-
for (let part in query.include) {
|
|
321
|
-
var includeEntity = query.include[part];
|
|
322
|
-
var $that = this;
|
|
323
|
-
if(includeEntity){
|
|
324
|
-
var parentObj = includeEntity[query.parentName];
|
|
325
|
-
var currentContext = "";
|
|
326
|
-
if(includeEntity.selectFields){
|
|
327
|
-
currentContext = context[tools.capitalizeFirstLetter(includeEntity.selectFields[0])];
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if(parentObj){
|
|
331
|
-
parentObj.entityMap = query.entityMap;
|
|
332
|
-
var foreignKey = $that.getForeignKey(entity.__name, currentContext.__entity);
|
|
333
|
-
var mainPrimaryKey = $that.getPrimarykey(entity);
|
|
334
|
-
var mainEntity = $that.getEntity(entity.__name, query.entityMap);
|
|
335
|
-
if(currentContext.__entity[entity.__name].type === "hasManyThrough"){
|
|
336
|
-
var foreignTable = tools.capitalizeFirstLetter(currentContext.__entity[entity.__name].foreignTable); //to uppercase letter
|
|
337
|
-
foreignKey = $that.getPrimarykey(currentContext.__entity);
|
|
338
|
-
mainPrimaryKey = context[foreignTable].__entity[currentContext.__entity.__name].foreignKey;
|
|
339
|
-
var mainEntity = $that.getEntity(foreignTable,query.entityMap);
|
|
340
|
-
}
|
|
341
|
-
// add foreign key to select so that it picks it up
|
|
342
|
-
if(parentObj.select){
|
|
343
|
-
parentObj.select.selectFields.push(foreignKey);
|
|
344
|
-
}else{
|
|
345
|
-
parentObj.select = {};
|
|
346
|
-
parentObj.select.selectFields = [];
|
|
347
|
-
parentObj.select.selectFields.push(foreignKey);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
var innerQuery = $that.buildQuery(parentObj, currentContext.__entity, context);
|
|
351
|
-
|
|
352
|
-
includeQueries.push(`LEFT JOIN (${innerQuery.query}) AS ${innerQuery.entity} ON ${ mainEntity}.${mainPrimaryKey} = ${innerQuery.entity}.${foreignKey}`);
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return includeQueries.join(' ');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
buildFrom(query, entity){
|
|
361
|
-
var entityName = this.getEntity(entity.__name, query.entityMap);
|
|
362
|
-
if(entityName ){
|
|
363
|
-
return `FROM ${entity.__name } AS ${entityName}`;
|
|
364
|
-
}
|
|
365
|
-
else{ return "" }
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
buildSelect(query, entity){
|
|
369
|
-
// this means that there is a select statement
|
|
370
|
-
var select = "SELECT";
|
|
371
|
-
const arr = [];
|
|
372
|
-
var $that = this;
|
|
373
|
-
if(query.select){
|
|
374
|
-
for (const item in query.select.selectFields) {
|
|
375
|
-
arr.push(`${$that.getEntity(entity.__name, query.entityMap)}.${query.select.selectFields[item]}`);
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
else{
|
|
380
|
-
var entityList = this.getEntityList(entity);
|
|
381
|
-
for (const item in entityList) {
|
|
382
|
-
arr.push(`${$that.getEntity(entity.__name, query.entityMap)}.${entityList[item]}`);
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
return `${select} ${arr.join(', ')} `;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
getForeignKey(name, entity){
|
|
389
|
-
if(entity && name){
|
|
390
|
-
return entity[name].foreignKey;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
getPrimarykey(entity){
|
|
395
|
-
for (const item in entity) {
|
|
396
|
-
if(entity[item].primary){
|
|
397
|
-
if(entity[item].primary === true){
|
|
398
|
-
return entity[item].name;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
getForeignTable(name, entity){
|
|
405
|
-
if(entity && name){
|
|
406
|
-
return entity[name].foreignTable;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
getInclude(name, query){
|
|
411
|
-
var include = query.include;
|
|
412
|
-
if(include){
|
|
413
|
-
for (let part in include) {
|
|
414
|
-
if(tools.capitalizeFirstLetter(include[part].selectFields[0]) === name){
|
|
415
|
-
return include[part];
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
else{
|
|
420
|
-
return "";
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
getEntity(name, maps){
|
|
425
|
-
for (let item in maps) {
|
|
426
|
-
var map = maps[item];
|
|
427
|
-
if(tools.capitalizeFirstLetter(name) === map.name){
|
|
428
|
-
return map.entity
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return "";
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// return a list of entity names and skip foreign keys and underscore.
|
|
435
|
-
getEntityList(entity){
|
|
436
|
-
var entitiesList = [];
|
|
437
|
-
var $that = this;
|
|
438
|
-
for (var ent in entity) {
|
|
439
|
-
if(!ent.startsWith("_")){
|
|
440
|
-
if(!entity[ent].foreignKey){
|
|
441
|
-
if(entity[ent].relationshipTable){
|
|
442
|
-
if($that.chechUnsupportedWords(entity[ent].relationshipTable)){
|
|
443
|
-
entitiesList.push(`'${entity[ent].relationshipTable}'`);
|
|
444
|
-
}
|
|
445
|
-
else{
|
|
446
|
-
entitiesList.push(entity[ent].relationshipTable);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
else{
|
|
450
|
-
if($that.chechUnsupportedWords(ent)){
|
|
451
|
-
entitiesList.push(`'${ent}'`);
|
|
452
|
-
}
|
|
453
|
-
else{
|
|
454
|
-
entitiesList.push(ent);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
else{
|
|
459
|
-
|
|
460
|
-
if(entity[ent].relationshipType === "belongsTo"){
|
|
461
|
-
var name = entity[ent].foreignKey;
|
|
462
|
-
if($that.chechUnsupportedWords(name)){
|
|
463
|
-
entitiesList.push(`'${name}'`);
|
|
464
|
-
//entitiesList.push(`'${ent}'`);
|
|
465
|
-
}
|
|
466
|
-
else{
|
|
467
|
-
entitiesList.push(name);
|
|
468
|
-
//entitiesList.push(ent);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// Ensure primary key is always included in SELECT list
|
|
476
|
-
try{
|
|
477
|
-
const pk = this.getPrimarykey(entity);
|
|
478
|
-
if(pk){
|
|
479
|
-
const hasPk = entitiesList.indexOf(pk) !== -1 || entitiesList.indexOf(`\`${pk}\``) !== -1;
|
|
480
|
-
if(!hasPk){ entitiesList.unshift(pk); }
|
|
481
|
-
}
|
|
482
|
-
}catch(_){ /* ignore */ }
|
|
483
|
-
return entitiesList
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
chechUnsupportedWords(word){
|
|
487
|
-
for (var item in this.unsupportedWords) {
|
|
488
|
-
var text = this.unsupportedWords[item];
|
|
489
|
-
if(text === word){
|
|
490
|
-
return true
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
startTransaction(){
|
|
497
|
-
this.db.prepare('BEGIN').run();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
endTransaction(){
|
|
501
|
-
this.db.prepare('COMMIT').run();
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
errorTransaction(){
|
|
505
|
-
this.db.prepare('ROLLBACK').run();
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
_buildSQLEqualTo(model){
|
|
509
|
-
var $that = this;
|
|
510
|
-
var argument = null;
|
|
511
|
-
var dirtyFields = model.__dirtyFields;
|
|
512
|
-
|
|
513
|
-
for (var column in dirtyFields) {
|
|
514
|
-
// Validate non-nullable constraints on updates
|
|
515
|
-
var fieldName = dirtyFields[column];
|
|
516
|
-
var entityDef = model.__entity[fieldName];
|
|
517
|
-
if(entityDef && entityDef.nullable === false && entityDef.primary !== true){
|
|
518
|
-
// Determine the value that will actually be persisted for this field
|
|
519
|
-
var persistedValue;
|
|
520
|
-
switch(entityDef.type){
|
|
521
|
-
case "integer":
|
|
522
|
-
persistedValue = model["_" + fieldName];
|
|
523
|
-
break;
|
|
524
|
-
case "belongsTo":
|
|
525
|
-
persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName];
|
|
526
|
-
break;
|
|
527
|
-
default:
|
|
528
|
-
persistedValue = model[fieldName];
|
|
529
|
-
}
|
|
530
|
-
var isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
|
|
531
|
-
if(persistedValue === undefined || persistedValue === null || isEmptyString){
|
|
532
|
-
throw `Entity ${model.__entity.__name} column ${fieldName} is a required Field`;
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
// TODO Boolean value is a string with a letter
|
|
536
|
-
var type = model.__entity[dirtyFields[column]].type;
|
|
537
|
-
|
|
538
|
-
if(model.__entity[dirtyFields[column]].relationshipType === "belongsTo"){
|
|
539
|
-
type = "belongsTo";
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
switch(type){
|
|
543
|
-
case "belongsTo" :
|
|
544
|
-
const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
|
|
545
|
-
let fkValue = model[dirtyFields[column]];
|
|
546
|
-
// 🔥 NEW: Validate foreign key type
|
|
547
|
-
try {
|
|
548
|
-
fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
549
|
-
} catch(typeError) {
|
|
550
|
-
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
551
|
-
}
|
|
552
|
-
argument = `${foreignKey} = ${fkValue},`;
|
|
553
|
-
break;
|
|
554
|
-
case "integer" :
|
|
555
|
-
const columneValue = model[`_${dirtyFields[column]}`];
|
|
556
|
-
var intValue = columneValue !== undefined ? columneValue : model[dirtyFields[column]];
|
|
557
|
-
// 🔥 NEW: Validate integer type
|
|
558
|
-
try {
|
|
559
|
-
intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
560
|
-
} catch(typeError) {
|
|
561
|
-
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
562
|
-
}
|
|
563
|
-
argument = argument === null ? `[${dirtyFields[column]}] = ${intValue},` : `${argument} [${dirtyFields[column]}] = ${intValue},`;
|
|
564
|
-
break;
|
|
565
|
-
case "string" :
|
|
566
|
-
var strValue = model[dirtyFields[column]];
|
|
567
|
-
// 🔥 NEW: Validate string type
|
|
568
|
-
try {
|
|
569
|
-
strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
570
|
-
} catch(typeError) {
|
|
571
|
-
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
572
|
-
}
|
|
573
|
-
argument = argument === null ? `${dirtyFields[column]} = '${$that._santizeSingleQuotes(strValue, { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',` : `${argument} ${dirtyFields[column]} = '${$that._santizeSingleQuotes(strValue, { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',`;
|
|
574
|
-
break;
|
|
575
|
-
case "time" :
|
|
576
|
-
// Always quote time values so empty strings remain valid ('')
|
|
577
|
-
var timeValue = model[dirtyFields[column]];
|
|
578
|
-
// 🔥 NEW: Validate time type
|
|
579
|
-
try {
|
|
580
|
-
timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
581
|
-
} catch(typeError) {
|
|
582
|
-
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
583
|
-
}
|
|
584
|
-
argument = argument === null ? `${dirtyFields[column]} = '${timeValue}',` : `${argument} ${dirtyFields[column]} = '${timeValue}',`;
|
|
585
|
-
break;
|
|
586
|
-
case "boolean" :
|
|
587
|
-
var boolValue = model[dirtyFields[column]];
|
|
588
|
-
// 🔥 NEW: Validate boolean type
|
|
589
|
-
try {
|
|
590
|
-
boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
591
|
-
} catch(typeError) {
|
|
592
|
-
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
593
|
-
}
|
|
594
|
-
argument = argument === null ? `${dirtyFields[column]} = '${this.boolType(boolValue)}',` : `${argument} ${dirtyFields[column]} = '${this.boolType(boolValue)}',`;
|
|
595
|
-
break;
|
|
596
|
-
default:
|
|
597
|
-
argument = argument === null ? `${dirtyFields[column]} = '${model[dirtyFields[column]]}',` : `${argument} ${dirtyFields[column]} = '${model[dirtyFields[column]]}',`;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if(argument){
|
|
601
|
-
return argument.replace(/,\s*$/, "");
|
|
602
|
-
}
|
|
603
|
-
else{
|
|
604
|
-
return -1;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
boolType(type){
|
|
609
|
-
var jj = String(type);
|
|
610
|
-
switch(jj) {
|
|
611
|
-
case "true":
|
|
612
|
-
return 1
|
|
613
|
-
break;
|
|
614
|
-
case "false":
|
|
615
|
-
return 0
|
|
616
|
-
break;
|
|
617
|
-
default:
|
|
618
|
-
return type;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
_buildDeleteObject(currentModel){
|
|
624
|
-
var primaryKey = currentModel.__Key === undefined ? tools.getPrimaryKeyObject(currentModel.__entity) : currentModel.__Key;
|
|
625
|
-
var value = currentModel.__value === undefined ? currentModel[primaryKey] : currentModel.__value;
|
|
626
|
-
var tableName = currentModel.__tableName === undefined ? currentModel.__entity.__name : currentModel.__tableName;
|
|
627
|
-
return {tableName: tableName, primaryKey : primaryKey, value : value};
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* NEW SECURE VERSION: Build SQL SET clause with parameterized queries (MySQL)
|
|
632
|
-
* Returns {sql: "column1 = ?, column2 = ?", params: [value1, value2]}
|
|
633
|
-
*/
|
|
634
|
-
_buildSQLEqualToParameterized(model){
|
|
635
|
-
var $that = this;
|
|
636
|
-
var sqlParts = [];
|
|
637
|
-
var params = [];
|
|
638
|
-
var dirtyFields = model.__dirtyFields;
|
|
639
|
-
|
|
640
|
-
for (var column in dirtyFields) {
|
|
641
|
-
var fieldName = dirtyFields[column];
|
|
642
|
-
var entityDef = model.__entity[fieldName];
|
|
643
|
-
if(entityDef && entityDef.nullable === false && entityDef.primary !== true){
|
|
644
|
-
var persistedValue;
|
|
645
|
-
switch(entityDef.type){
|
|
646
|
-
case "integer": persistedValue = model["_" + fieldName]; break;
|
|
647
|
-
case "belongsTo": persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName]; break;
|
|
648
|
-
default: persistedValue = model[fieldName];
|
|
649
|
-
}
|
|
650
|
-
var isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
|
|
651
|
-
if(persistedValue === undefined || persistedValue === null || isEmptyString){
|
|
652
|
-
throw `Entity ${model.__entity.__name} column ${fieldName} is a required Field`;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
var type = model.__entity[dirtyFields[column]].type;
|
|
657
|
-
if(model.__entity[dirtyFields[column]].relationshipType === "belongsTo"){ type = "belongsTo"; }
|
|
658
|
-
|
|
659
|
-
switch(type){
|
|
660
|
-
case "belongsTo":
|
|
661
|
-
const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
|
|
662
|
-
let fkValue = model[dirtyFields[column]];
|
|
663
|
-
// 🔥 Apply toDatabase transformer
|
|
664
|
-
try { fkValue = FieldTransformer.toDatabase(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
665
|
-
catch(transformError) { throw new Error(`UPDATE failed: ${transformError.message}`); }
|
|
666
|
-
try { fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
667
|
-
catch(typeError) { throw new Error(`UPDATE failed: ${typeError.message}`); }
|
|
668
|
-
var fore = `_${dirtyFields[column]}`;
|
|
669
|
-
sqlParts.push(`${foreignKey} = ?`);
|
|
670
|
-
params.push(model[fore]);
|
|
671
|
-
break;
|
|
672
|
-
case "integer":
|
|
673
|
-
var intValue = model["_" + dirtyFields[column]];
|
|
674
|
-
// 🔥 Apply toDatabase transformer
|
|
675
|
-
try { intValue = FieldTransformer.toDatabase(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
676
|
-
catch(transformError) { throw new Error(`UPDATE failed: ${transformError.message}`); }
|
|
677
|
-
try { intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
678
|
-
catch(typeError) { throw new Error(`UPDATE failed: ${typeError.message}`); }
|
|
679
|
-
sqlParts.push(`${dirtyFields[column]} = ?`);
|
|
680
|
-
params.push(intValue);
|
|
681
|
-
break;
|
|
682
|
-
case "string":
|
|
683
|
-
var strValue = model[dirtyFields[column]];
|
|
684
|
-
// 🔥 Apply toDatabase transformer
|
|
685
|
-
try { strValue = FieldTransformer.toDatabase(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
686
|
-
catch(transformError) { throw new Error(`UPDATE failed: ${transformError.message}`); }
|
|
687
|
-
try { strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
688
|
-
catch(typeError) { throw new Error(`UPDATE failed: ${typeError.message}`); }
|
|
689
|
-
sqlParts.push(`${dirtyFields[column]} = ?`);
|
|
690
|
-
params.push(strValue);
|
|
691
|
-
break;
|
|
692
|
-
case "boolean":
|
|
693
|
-
var boolValue = model[dirtyFields[column]];
|
|
694
|
-
// 🔥 Apply toDatabase transformer
|
|
695
|
-
try { boolValue = FieldTransformer.toDatabase(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
696
|
-
catch(transformError) { throw new Error(`UPDATE failed: ${transformError.message}`); }
|
|
697
|
-
try { boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
698
|
-
catch(typeError) { throw new Error(`UPDATE failed: ${typeError.message}`); }
|
|
699
|
-
boolValue = $that._convertValueForDatabase(boolValue, model.__entity[dirtyFields[column]].type);
|
|
700
|
-
var bool = model.__entity[dirtyFields[column]].valueConversion ? tools.convertBooleanToNumber(boolValue) : boolValue;
|
|
701
|
-
sqlParts.push(`${dirtyFields[column]} = ?`);
|
|
702
|
-
params.push(bool);
|
|
703
|
-
break;
|
|
704
|
-
case "time":
|
|
705
|
-
var timeValue = model[dirtyFields[column]];
|
|
706
|
-
// 🔥 Apply toDatabase transformer
|
|
707
|
-
try { timeValue = FieldTransformer.toDatabase(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
708
|
-
catch(transformError) { throw new Error(`UPDATE failed: ${transformError.message}`); }
|
|
709
|
-
try { timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
|
|
710
|
-
catch(typeError) { throw new Error(`UPDATE failed: ${typeError.message}`); }
|
|
711
|
-
sqlParts.push(`${dirtyFields[column]} = ?`);
|
|
712
|
-
params.push(timeValue);
|
|
713
|
-
break;
|
|
714
|
-
case "hasMany":
|
|
715
|
-
sqlParts.push(`${dirtyFields[column]} = ?`);
|
|
716
|
-
params.push(model[dirtyFields[column]]);
|
|
717
|
-
break;
|
|
718
|
-
default:
|
|
719
|
-
sqlParts.push(`${dirtyFields[column]} = ?`);
|
|
720
|
-
params.push(model[dirtyFields[column]]);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
return sqlParts.length > 0 ? { sql: sqlParts.join(', '), params: params } : -1;
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* NEW SECURE VERSION: Build SQL INSERT with parameterized queries (MySQL)
|
|
729
|
-
* Returns {tableName, columns, placeholders, params}
|
|
730
|
-
*/
|
|
731
|
-
_buildSQLInsertObjectParameterized(fields, modelEntity){
|
|
732
|
-
var $that = this;
|
|
733
|
-
var columnNames = [];
|
|
734
|
-
var params = [];
|
|
735
|
-
|
|
736
|
-
for (var column in modelEntity) {
|
|
737
|
-
if(column.indexOf("__") === -1 ){
|
|
738
|
-
var fieldColumn = fields[column];
|
|
739
|
-
|
|
740
|
-
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
741
|
-
// 🔥 Apply toDatabase transformer before validation
|
|
742
|
-
try { fieldColumn = FieldTransformer.toDatabase(fieldColumn, modelEntity[column], modelEntity.__name, column); }
|
|
743
|
-
catch(transformError) { throw new Error(`INSERT failed: ${transformError.message}`); }
|
|
744
|
-
|
|
745
|
-
try { fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column); }
|
|
746
|
-
catch(typeError) { throw new Error(`INSERT failed: ${typeError.message}`); }
|
|
747
|
-
|
|
748
|
-
fieldColumn = $that._convertValueForDatabase(fieldColumn, modelEntity[column].type);
|
|
749
|
-
|
|
750
|
-
var relationship = modelEntity[column].relationshipType;
|
|
751
|
-
var actualColumn = relationship === "belongsTo" ? modelEntity[column].foreignKey : column;
|
|
752
|
-
columnNames.push(actualColumn);
|
|
753
|
-
params.push(fieldColumn);
|
|
754
|
-
}
|
|
755
|
-
else{
|
|
756
|
-
switch(modelEntity[column].type){
|
|
757
|
-
case "belongsTo":
|
|
758
|
-
var fieldObject = tools.findTrackedObject(fields.__context.__trackedEntities, column);
|
|
759
|
-
if(Object.keys(fieldObject).length > 0){
|
|
760
|
-
var primaryKey = tools.getPrimaryKeyObject(fieldObject.__entity);
|
|
761
|
-
fieldColumn = fieldObject[primaryKey];
|
|
762
|
-
var actualColumn = modelEntity[column].foreignKey;
|
|
763
|
-
columnNames.push(actualColumn);
|
|
764
|
-
params.push(fieldColumn);
|
|
765
|
-
}
|
|
766
|
-
break;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
if(columnNames.length > 0){
|
|
773
|
-
var placeholders = params.map(() => '?').join(', ');
|
|
774
|
-
return { tableName: modelEntity.__name, columns: columnNames.join(', '), placeholders: placeholders, params: params };
|
|
775
|
-
} else {
|
|
776
|
-
return -1;
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Convert validated value to database-specific format
|
|
782
|
-
* Modern ORM pattern: transparent database-specific conversions
|
|
783
|
-
*
|
|
784
|
-
* @param {*} value - Already validated value
|
|
785
|
-
* @param {string} fieldType - Field type from entity definition
|
|
786
|
-
* @returns {*} Database-ready value
|
|
787
|
-
*/
|
|
788
|
-
_convertValueForDatabase(value, fieldType){
|
|
789
|
-
if(value === undefined || value === null){
|
|
790
|
-
return value;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// MySQL boolean conversion: JavaScript boolean → TINYINT (1/0)
|
|
794
|
-
if(fieldType === 'boolean' && typeof value === 'boolean'){
|
|
795
|
-
return value ? 1 : 0;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
return value;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
/**
|
|
802
|
-
* Validate and coerce field value to match entity type definition
|
|
803
|
-
* Throws detailed error if type cannot be coerced
|
|
804
|
-
* @param {*} value - The field value to validate
|
|
805
|
-
* @param {object} entityDef - The entity definition for this field
|
|
806
|
-
* @param {string} entityName - Name of the entity (for error messages)
|
|
807
|
-
* @param {string} fieldName - Name of the field (for error messages)
|
|
808
|
-
* @returns {*} - The validated/coerced value
|
|
809
|
-
*/
|
|
810
|
-
_validateAndCoerceFieldType(value, entityDef, entityName, fieldName){
|
|
811
|
-
if(value === undefined || value === null){
|
|
812
|
-
return value; // Let nullable validation handle this
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const expectedType = entityDef.type;
|
|
816
|
-
const actualType = typeof value;
|
|
817
|
-
|
|
818
|
-
switch(expectedType){
|
|
819
|
-
case "integer":
|
|
820
|
-
// Coerce to integer if possible
|
|
821
|
-
if(actualType === 'number'){
|
|
822
|
-
if(!Number.isInteger(value)){
|
|
823
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Expected integer but got float ${value}, rounding to ${Math.round(value)}`);
|
|
824
|
-
return Math.round(value);
|
|
825
|
-
}
|
|
826
|
-
return value;
|
|
827
|
-
}
|
|
828
|
-
if(actualType === 'string'){
|
|
829
|
-
const parsed = parseInt(value, 10);
|
|
830
|
-
if(isNaN(parsed)){
|
|
831
|
-
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got string "${value}" which cannot be converted to a number`);
|
|
832
|
-
}
|
|
833
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to integer ${parsed}`);
|
|
834
|
-
return parsed;
|
|
835
|
-
}
|
|
836
|
-
if(actualType === 'boolean'){
|
|
837
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting boolean ${value} to integer ${value ? 1 : 0}`);
|
|
838
|
-
return value ? 1 : 0;
|
|
839
|
-
}
|
|
840
|
-
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
841
|
-
|
|
842
|
-
case "string":
|
|
843
|
-
// Coerce to string
|
|
844
|
-
if(actualType === 'string'){
|
|
845
|
-
return value;
|
|
846
|
-
}
|
|
847
|
-
// Allow auto-conversion from primitives
|
|
848
|
-
if(['number', 'boolean'].includes(actualType)){
|
|
849
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting ${actualType} ${value} to string "${String(value)}"`);
|
|
850
|
-
return String(value);
|
|
851
|
-
}
|
|
852
|
-
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected string, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
853
|
-
|
|
854
|
-
case "boolean":
|
|
855
|
-
case "bool":
|
|
856
|
-
if (typeof value === 'boolean') return value;
|
|
857
|
-
if (value === 1 || value === '1' || value === 'true' || value === true) return true;
|
|
858
|
-
if (value === 0 || value === '0' || value === 'false' || value === false) return false;
|
|
859
|
-
throw new Error(`Invalid boolean value: ${value}`);
|
|
860
|
-
|
|
861
|
-
case "time":
|
|
862
|
-
// Time fields should be strings or timestamps
|
|
863
|
-
if(actualType === 'string' || actualType === 'number'){
|
|
864
|
-
return value;
|
|
865
|
-
}
|
|
866
|
-
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected time (string/number), got ${actualType} with value ${JSON.stringify(value)}`);
|
|
867
|
-
|
|
868
|
-
default:
|
|
869
|
-
// For unknown types, allow the value through but warn
|
|
870
|
-
if(actualType === 'object'){
|
|
871
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Setting object value for type "${expectedType}". This may cause issues.`);
|
|
872
|
-
}
|
|
873
|
-
return value;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
// return columns and value strings
|
|
879
|
-
_buildSQLInsertObject(fields, modelEntity){
|
|
880
|
-
var $that = this;
|
|
881
|
-
var columns = null;
|
|
882
|
-
var values = null;
|
|
883
|
-
for (var column in modelEntity) {
|
|
884
|
-
// column1 = value1, column2 = value2, ...
|
|
885
|
-
if(column.indexOf("__") === -1 ){
|
|
886
|
-
// call the get method if avlable
|
|
887
|
-
var fieldColumn = "";
|
|
888
|
-
// check if get function is avaliable if so use that
|
|
889
|
-
fieldColumn = fields[column];
|
|
890
|
-
|
|
891
|
-
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
892
|
-
// 🔥 NEW: Validate and coerce field type before processing
|
|
893
|
-
try {
|
|
894
|
-
fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
895
|
-
} catch(typeError) {
|
|
896
|
-
throw new Error(`INSERT failed: ${typeError.message}`);
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
switch(modelEntity[column].type){
|
|
900
|
-
case "string" :
|
|
901
|
-
fieldColumn = `'${$that._santizeSingleQuotes(fieldColumn, { entityName: modelEntity.__name, fieldName: column })}'`;
|
|
902
|
-
break;
|
|
903
|
-
case "time" :
|
|
904
|
-
// Quote time values to prevent blank values from producing malformed SQL
|
|
905
|
-
fieldColumn = `'${fieldColumn}'`;
|
|
906
|
-
break;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
var relationship = modelEntity[column].relationshipType
|
|
910
|
-
if(relationship === "belongsTo"){
|
|
911
|
-
column = modelEntity[column].foreignKey
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
// Use backtick-quoted identifiers for MySQL column names
|
|
916
|
-
columns = columns === null ? `\`${column}\`,` : `${columns} \`${column}\`,`;
|
|
917
|
-
values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
|
|
918
|
-
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
return {tableName: modelEntity.__name, columns: columns.replace(/,\s*$/, ""), values: values.replace(/,\s*$/, "")};
|
|
924
|
-
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
// will add double single quotes to allow string to be saved.
|
|
928
|
-
_santizeSingleQuotes(value, context){
|
|
929
|
-
if (typeof value === 'string' || value instanceof String){
|
|
930
|
-
return value.replace(/'/g, "''");
|
|
931
|
-
}
|
|
932
|
-
else{
|
|
933
|
-
var details = context || {};
|
|
934
|
-
var entityName = details.entityName || 'UnknownEntity';
|
|
935
|
-
var fieldName = details.fieldName || 'UnknownField';
|
|
936
|
-
var valueType = (value === null) ? 'null' : (value === undefined ? 'undefined' : typeof value);
|
|
937
|
-
var preview;
|
|
938
|
-
try{ preview = (value === null || value === undefined) ? String(value) : JSON.stringify(value); }
|
|
939
|
-
catch(_){ preview = '[unserializable]'; }
|
|
940
|
-
if(preview && preview.length > 120){ preview = preview.substring(0, 120) + '…'; }
|
|
941
|
-
var message = `Field is not a string: entity=${entityName}, field=${fieldName}, type=${valueType}, value=${preview}`;
|
|
942
|
-
console.error(message);
|
|
943
|
-
throw new Error(message);
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// converts any object into SQL parameter select string
|
|
948
|
-
_convertEntityToSelectParameterString(obj, entityName){
|
|
949
|
-
// todo: loop throgh object and append string with comma to
|
|
950
|
-
var mainString = "";
|
|
951
|
-
const entries = Object.keys(obj);
|
|
952
|
-
|
|
953
|
-
for (const [name] of entries) {
|
|
954
|
-
mainString += `${mainString}, ${entityName}.${name}`;
|
|
955
|
-
}
|
|
956
|
-
return mainString;;
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
_execute(query){
|
|
960
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
961
|
-
console.debug("[SQL]", query);
|
|
962
|
-
}
|
|
963
|
-
try{
|
|
964
|
-
this.db.connect(this.db);
|
|
965
|
-
const res = this.db.query(query);
|
|
966
|
-
if(res === null){
|
|
967
|
-
const dbName = (this.db && this.db.config && this.db.config.database) ? this.db.config.database : '(unknown)';
|
|
968
|
-
if(this.db && this.db.lastErrorCode === 'ER_BAD_DB_ERROR'){
|
|
969
|
-
console.error(`MySQL execute skipped: database '${dbName}' does not exist`);
|
|
970
|
-
}else{
|
|
971
|
-
console.error('MySQL execute skipped: connection not defined');
|
|
972
|
-
}
|
|
973
|
-
return null;
|
|
974
|
-
}
|
|
975
|
-
return res;
|
|
976
|
-
}catch(err){
|
|
977
|
-
const code = err && err.code ? err.code : '';
|
|
978
|
-
if(code === 'ER_BAD_DB_ERROR' || code === 'ECONNREFUSED' || code === 'PROTOCOL_CONNECTION_LOST'){
|
|
979
|
-
const dbName = (this.db && this.db.config && this.db.config.database) ? this.db.config.database : '(unknown)';
|
|
980
|
-
if(code === 'ER_BAD_DB_ERROR'){
|
|
981
|
-
console.error(`MySQL execute skipped: database '${dbName}' does not exist`);
|
|
982
|
-
} else {
|
|
983
|
-
console.error('MySQL execute skipped: connection not defined');
|
|
984
|
-
}
|
|
985
|
-
return null;
|
|
986
|
-
}
|
|
987
|
-
console.error(err);
|
|
988
|
-
return null;
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
_run(query){
|
|
993
|
-
try{
|
|
994
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
995
|
-
console.debug("[SQL]", query);
|
|
996
|
-
}
|
|
997
|
-
this.db.connect(this.db);
|
|
998
|
-
const result = this.db.query(query);
|
|
999
|
-
|
|
1000
|
-
return result;}
|
|
1001
|
-
catch (error) {
|
|
1002
|
-
console.error(error);
|
|
1003
|
-
// Expected output: ReferenceError: nonExistentFunction is not defined
|
|
1004
|
-
// (Note: the exact output may be browser-dependent)
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* NEW SECURE VERSION: Execute query with parameters
|
|
1010
|
-
* Prevents SQL injection by using parameterized queries
|
|
1011
|
-
*/
|
|
1012
|
-
_executeWithParams(query, params = []){
|
|
1013
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
1014
|
-
console.debug("[SQL]", query);
|
|
1015
|
-
console.debug("[Params]", params);
|
|
1016
|
-
}
|
|
1017
|
-
try{
|
|
1018
|
-
this.db.connect(this.db);
|
|
1019
|
-
const res = this.db.query(query, params);
|
|
1020
|
-
if(res === null){
|
|
1021
|
-
const dbName = (this.db && this.db.config && this.db.config.database) ? this.db.config.database : '(unknown)';
|
|
1022
|
-
if(this.db && this.db.lastErrorCode === 'ER_BAD_DB_ERROR'){
|
|
1023
|
-
console.error(`MySQL execute skipped: database '${dbName}' does not exist`);
|
|
1024
|
-
}else{
|
|
1025
|
-
console.error('MySQL execute skipped: connection not defined');
|
|
1026
|
-
}
|
|
1027
|
-
return null;
|
|
1028
|
-
}
|
|
1029
|
-
return res;
|
|
1030
|
-
}catch(err){
|
|
1031
|
-
const code = err && err.code ? err.code : '';
|
|
1032
|
-
if(code === 'ER_BAD_DB_ERROR' || code === 'ECONNREFUSED' || code === 'PROTOCOL_CONNECTION_LOST'){
|
|
1033
|
-
const dbName = (this.db && this.db.config && this.db.config.database) ? this.db.config.database : '(unknown)';
|
|
1034
|
-
if(code === 'ER_BAD_DB_ERROR'){
|
|
1035
|
-
console.error(`MySQL execute skipped: database '${dbName}' does not exist`);
|
|
1036
|
-
} else {
|
|
1037
|
-
console.error('MySQL execute skipped: connection not defined');
|
|
1038
|
-
}
|
|
1039
|
-
return null;
|
|
1040
|
-
}
|
|
1041
|
-
console.error(err);
|
|
1042
|
-
return null;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* NEW SECURE VERSION: Run query with parameters
|
|
1048
|
-
* Prevents SQL injection by using parameterized queries
|
|
1049
|
-
*/
|
|
1050
|
-
_runWithParams(query, params = []){
|
|
1051
|
-
try{
|
|
1052
|
-
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
1053
|
-
console.debug("[SQL]", query);
|
|
1054
|
-
console.debug("[Params]", params);
|
|
1055
|
-
}
|
|
1056
|
-
this.db.connect(this.db);
|
|
1057
|
-
const result = this.db.query(query, params);
|
|
1058
|
-
return result;
|
|
1059
|
-
}
|
|
1060
|
-
catch (error) {
|
|
1061
|
-
console.error(error);
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
setDB(db, type){
|
|
1066
|
-
this.db = db;
|
|
1067
|
-
this.dbType = type; // this will let us know which type of sqlengine to use.
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
module.exports = SQLLiteEngine;
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
/***
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
* const mysql = require('mysql2/promise');
|
|
1081
|
-
|
|
1082
|
-
class MySQLClient {
|
|
1083
|
-
constructor(config) {
|
|
1084
|
-
this.config = config;
|
|
1085
|
-
this.pool = mysql.createPool(config);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
async query(sql, params = []) {
|
|
1089
|
-
const connection = await this.pool.getConnection();
|
|
1090
|
-
try {
|
|
1091
|
-
const [results] = await connection.execute(sql, params);
|
|
1092
|
-
return results;
|
|
1093
|
-
} finally {
|
|
1094
|
-
connection.release();
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
async close() {
|
|
1099
|
-
await this.pool.end();
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
module.exports = MySQLClient;
|
|
1104
|
-
|
|
1105
|
-
*/
|