masterrecord 0.3.16 → 0.3.18

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/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
- */