masterrecord 0.2.36 → 0.3.1
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/.claude/settings.local.json +20 -1
- package/Entity/entityModel.js +6 -0
- package/Entity/entityTrackerModel.js +20 -3
- package/Entity/fieldTransformer.js +266 -0
- package/Migrations/migrationMySQLQuery.js +145 -1
- package/Migrations/migrationPostgresQuery.js +402 -0
- package/Migrations/migrationSQLiteQuery.js +145 -1
- package/Migrations/schema.js +131 -28
- package/QueryLanguage/queryMethods.js +193 -15
- package/QueryLanguage/queryParameters.js +136 -0
- package/QueryLanguage/queryScript.js +13 -4
- package/SQLLiteEngine.js +331 -20
- package/context.js +91 -14
- package/docs/INCLUDES_CLARIFICATION.md +202 -0
- package/docs/METHODS_REFERENCE.md +184 -0
- package/docs/MIGRATIONS_GUIDE.md +699 -0
- package/docs/POSTGRESQL_SETUP.md +415 -0
- package/examples/jsonArrayTransformer.js +215 -0
- package/mySQLEngine.js +273 -17
- package/package.json +3 -3
- package/postgresEngine.js +600 -483
- package/postgresSyncConnect.js +209 -0
- package/readme.md +1046 -416
- package/test/anyCommaStringTest.js +237 -0
- package/test/anyMethodTest.js +176 -0
- package/test/findByIdTest.js +227 -0
- package/test/includesFeatureTest.js +183 -0
- package/test/includesTransformTest.js +110 -0
- package/test/newMethodTest.js +330 -0
- package/test/newMethodUnitTest.js +320 -0
- package/test/parameterizedPlaceholderTest.js +159 -0
- package/test/postgresEngineTest.js +463 -0
- package/test/postgresIntegrationTest.js +381 -0
- package/test/securityTest.js +268 -0
- package/test/singleDollarPlaceholderTest.js +238 -0
- package/test/transformerTest.js +287 -0
- package/test/verifyFindById.js +169 -0
- package/test/verifyNewMethod.js +191 -0
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// version 0.0.
|
|
1
|
+
// version 0.0.9
|
|
2
2
|
|
|
3
3
|
const LOG_OPERATORS_REGEX = /(\|\|)|(&&)/;
|
|
4
4
|
var tools = require('../Tools');
|
|
5
|
+
const QueryParameters = require('./queryParameters');
|
|
5
6
|
|
|
6
7
|
class queryScript{
|
|
7
8
|
|
|
8
|
-
constructor(){
|
|
9
|
+
constructor(){
|
|
10
|
+
this.parameters = new QueryParameters();
|
|
11
|
+
// Initialize script.parameters reference
|
|
12
|
+
this.script.parameters = this.parameters;
|
|
13
|
+
}
|
|
9
14
|
|
|
10
15
|
script = {
|
|
11
16
|
select : false,
|
|
@@ -19,11 +24,13 @@ class queryScript{
|
|
|
19
24
|
skip: 0,
|
|
20
25
|
orderBy : false,
|
|
21
26
|
orderByDesc : false,
|
|
22
|
-
parentName : ""
|
|
27
|
+
parentName : "",
|
|
28
|
+
parameters: null // Will hold QueryParameters instance
|
|
23
29
|
};
|
|
24
30
|
|
|
25
31
|
|
|
26
32
|
reset(){
|
|
33
|
+
this.parameters.reset();
|
|
27
34
|
this.script = {
|
|
28
35
|
select : false,
|
|
29
36
|
where: false,
|
|
@@ -35,7 +42,9 @@ class queryScript{
|
|
|
35
42
|
take : 0,
|
|
36
43
|
skip: 0,
|
|
37
44
|
orderBy : false,
|
|
38
|
-
orderByDesc : false
|
|
45
|
+
orderByDesc : false,
|
|
46
|
+
parentName : "",
|
|
47
|
+
parameters: this.parameters
|
|
39
48
|
};
|
|
40
49
|
}
|
|
41
50
|
|
package/SQLLiteEngine.js
CHANGED
|
@@ -1,28 +1,46 @@
|
|
|
1
1
|
// Version 0.0.23
|
|
2
2
|
var tools = require('masterrecord/Tools');
|
|
3
|
+
var FieldTransformer = require('masterrecord/Entity/fieldTransformer');
|
|
3
4
|
|
|
4
5
|
class SQLLiteEngine {
|
|
5
6
|
|
|
6
7
|
unsupportedWords = ["order"]
|
|
7
8
|
|
|
8
9
|
update(query){
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Use parameterized query for security
|
|
11
|
+
// query.arg now contains {sql, params} from _buildSQLEqualToParameterized
|
|
12
|
+
if(query.arg && typeof query.arg === 'object' && query.arg.sql && query.arg.params){
|
|
13
|
+
var sqlQuery = ` UPDATE [${query.tableName}]
|
|
14
|
+
SET ${query.arg.sql}
|
|
15
|
+
WHERE [${query.tableName}].[${query.primaryKey}] = ?`;
|
|
16
|
+
// Add primaryKeyValue to params array
|
|
17
|
+
var params = [...query.arg.params, query.primaryKeyValue];
|
|
18
|
+
return this._runWithParams(sqlQuery, params);
|
|
19
|
+
} else {
|
|
20
|
+
// Fallback to old method (for backwards compatibility during migration)
|
|
21
|
+
var sqlQuery = ` UPDATE [${query.tableName}]
|
|
22
|
+
SET ${query.arg}
|
|
23
|
+
WHERE [${query.tableName}].[${query.primaryKey}] = ?`;
|
|
24
|
+
return this._runWithParams(sqlQuery, [query.primaryKeyValue]);
|
|
25
|
+
}
|
|
13
26
|
}
|
|
14
27
|
|
|
15
28
|
delete(queryObject){
|
|
16
29
|
var sqlObject = this._buildDeleteObject(queryObject);
|
|
17
|
-
|
|
18
|
-
|
|
30
|
+
// Use parameterized query to prevent SQL injection
|
|
31
|
+
var sqlQuery = `DELETE FROM [${sqlObject.tableName}] WHERE [${sqlObject.tableName}].[${sqlObject.primaryKey}] = ?`;
|
|
32
|
+
return this._executeWithParams(sqlQuery, [sqlObject.value]);
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
insert(queryObject){
|
|
22
|
-
|
|
36
|
+
// Use NEW SECURE parameterized version
|
|
37
|
+
var sqlObject = this._buildSQLInsertObjectParameterized(queryObject, queryObject.__entity);
|
|
38
|
+
if(sqlObject === -1){
|
|
39
|
+
throw new Error('INSERT failed: No columns to insert');
|
|
40
|
+
}
|
|
23
41
|
var query = `INSERT INTO [${sqlObject.tableName}] (${sqlObject.columns})
|
|
24
|
-
VALUES (${sqlObject.
|
|
25
|
-
var queryObj = this.
|
|
42
|
+
VALUES (${sqlObject.placeholders})`;
|
|
43
|
+
var queryObj = this._runWithParams(query, sqlObject.params);
|
|
26
44
|
var open = {
|
|
27
45
|
"id": queryObj.lastInsertRowid
|
|
28
46
|
};
|
|
@@ -44,8 +62,11 @@ class SQLLiteEngine {
|
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
64
|
if(queryString.query){
|
|
65
|
+
// Get parameters from query script
|
|
66
|
+
const params = query.parameters ? query.parameters.getParams() : [];
|
|
47
67
|
console.log("SQL:", queryString.query);
|
|
48
|
-
|
|
68
|
+
console.log("Params:", params);
|
|
69
|
+
var queryReturn = this.db.prepare(queryString.query).get(...params);
|
|
49
70
|
return queryReturn;
|
|
50
71
|
}
|
|
51
72
|
return null;
|
|
@@ -58,8 +79,9 @@ class SQLLiteEngine {
|
|
|
58
79
|
// Introspection helpers
|
|
59
80
|
tableExists(tableName){
|
|
60
81
|
try{
|
|
61
|
-
|
|
62
|
-
const
|
|
82
|
+
// Use parameterized query to prevent SQL injection
|
|
83
|
+
const sql = `SELECT name FROM sqlite_master WHERE type='table' AND name=?`;
|
|
84
|
+
const row = this.db.prepare(sql).get(tableName);
|
|
63
85
|
return !!row;
|
|
64
86
|
}catch(_){ return false; }
|
|
65
87
|
}
|
|
@@ -88,8 +110,11 @@ class SQLLiteEngine {
|
|
|
88
110
|
}
|
|
89
111
|
if(queryString.query){
|
|
90
112
|
var queryCount = queryString.query
|
|
113
|
+
// Get parameters from query script
|
|
114
|
+
const params = query.parameters ? query.parameters.getParams() : [];
|
|
91
115
|
console.log("SQL:", queryCount );
|
|
92
|
-
|
|
116
|
+
console.log("Params:", params);
|
|
117
|
+
var queryReturn = this.db.prepare(queryCount).get(...params);
|
|
93
118
|
return queryReturn;
|
|
94
119
|
}
|
|
95
120
|
return null;
|
|
@@ -110,8 +135,11 @@ class SQLLiteEngine {
|
|
|
110
135
|
selectQuery = this.buildQuery(query, entity, context);
|
|
111
136
|
}
|
|
112
137
|
if(selectQuery.query){
|
|
138
|
+
// Get parameters from query script
|
|
139
|
+
const params = query.parameters ? query.parameters.getParams() : [];
|
|
113
140
|
console.log("SQL:", selectQuery.query);
|
|
114
|
-
|
|
141
|
+
console.log("Params:", params);
|
|
142
|
+
var queryReturn = this.db.prepare(selectQuery.query).all(...params);
|
|
115
143
|
return queryReturn;
|
|
116
144
|
}
|
|
117
145
|
return null;
|
|
@@ -247,16 +275,28 @@ class SQLLiteEngine {
|
|
|
247
275
|
if(item.expressions[exp].arg === "null"){
|
|
248
276
|
strQuery = `${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
|
|
249
277
|
}else{
|
|
250
|
-
|
|
278
|
+
// Check if arg is a parameterized placeholder
|
|
279
|
+
var isPlaceholder = (item.expressions[exp].arg === '?' || /^\$\d+$/.test(item.expressions[exp].arg));
|
|
280
|
+
if(isPlaceholder){
|
|
281
|
+
strQuery = `${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
|
|
282
|
+
}else{
|
|
283
|
+
strQuery = `${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
|
|
284
|
+
}
|
|
251
285
|
}
|
|
252
286
|
}
|
|
253
287
|
else{
|
|
254
288
|
if(item.expressions[exp].arg === "null"){
|
|
255
289
|
strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
|
|
256
290
|
}else{
|
|
257
|
-
|
|
291
|
+
// Check if arg is a parameterized placeholder
|
|
292
|
+
var isPlaceholder = (item.expressions[exp].arg === '?' || /^\$\d+$/.test(item.expressions[exp].arg));
|
|
293
|
+
if(isPlaceholder){
|
|
294
|
+
strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
|
|
295
|
+
}else{
|
|
296
|
+
strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
|
|
297
|
+
}
|
|
258
298
|
}
|
|
259
|
-
|
|
299
|
+
|
|
260
300
|
}
|
|
261
301
|
}
|
|
262
302
|
andList.push(strQuery);
|
|
@@ -313,6 +353,12 @@ class SQLLiteEngine {
|
|
|
313
353
|
if(func === "IN"){
|
|
314
354
|
return `${ent}.${field} ${func} ${arg}`;
|
|
315
355
|
}
|
|
356
|
+
// Check if arg is a parameterized placeholder (? for MySQL/SQLite, $1/$2/etc for Postgres)
|
|
357
|
+
var isPlaceholder = (arg === '?' || /^\$\d+$/.test(arg));
|
|
358
|
+
if(isPlaceholder){
|
|
359
|
+
// Don't quote placeholders - they must remain as bare ? or $1
|
|
360
|
+
return `${ent}.${field} ${func} ${arg}`;
|
|
361
|
+
}
|
|
316
362
|
return `${ent}.${field} ${func} '${arg}'`;
|
|
317
363
|
}
|
|
318
364
|
|
|
@@ -648,10 +694,169 @@ class SQLLiteEngine {
|
|
|
648
694
|
else{
|
|
649
695
|
return -1;
|
|
650
696
|
}
|
|
651
|
-
|
|
697
|
+
|
|
652
698
|
}
|
|
653
699
|
|
|
654
|
-
|
|
700
|
+
/**
|
|
701
|
+
* NEW SECURE VERSION: Build SQL SET clause with parameterized queries
|
|
702
|
+
* Returns {sql: "column1 = ?, column2 = ?", params: [value1, value2]}
|
|
703
|
+
* This prevents SQL injection by separating SQL structure from values
|
|
704
|
+
*/
|
|
705
|
+
_buildSQLEqualToParameterized(model){
|
|
706
|
+
var $that = this;
|
|
707
|
+
var sqlParts = [];
|
|
708
|
+
var params = [];
|
|
709
|
+
var dirtyFields = model.__dirtyFields;
|
|
710
|
+
|
|
711
|
+
for (var column in dirtyFields) {
|
|
712
|
+
// Validate non-nullable constraints on updates
|
|
713
|
+
var fieldName = dirtyFields[column];
|
|
714
|
+
var entityDef = model.__entity[fieldName];
|
|
715
|
+
if(entityDef && entityDef.nullable === false && entityDef.primary !== true){
|
|
716
|
+
// Determine the value that will actually be persisted for this field
|
|
717
|
+
var persistedValue;
|
|
718
|
+
switch(entityDef.type){
|
|
719
|
+
case "integer":
|
|
720
|
+
persistedValue = model["_" + fieldName];
|
|
721
|
+
break;
|
|
722
|
+
case "belongsTo":
|
|
723
|
+
persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName];
|
|
724
|
+
break;
|
|
725
|
+
default:
|
|
726
|
+
persistedValue = model[fieldName];
|
|
727
|
+
}
|
|
728
|
+
var isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
|
|
729
|
+
if(persistedValue === undefined || persistedValue === null || isEmptyString){
|
|
730
|
+
throw `Entity ${model.__entity.__name} column ${fieldName} is a required Field`;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
var type = model.__entity[dirtyFields[column]].type;
|
|
735
|
+
|
|
736
|
+
if(model.__entity[dirtyFields[column]].relationshipType === "belongsTo"){
|
|
737
|
+
type = "belongsTo";
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Build parameterized SET clause
|
|
741
|
+
switch(type){
|
|
742
|
+
case "belongsTo":
|
|
743
|
+
const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
|
|
744
|
+
let fkValue = model[dirtyFields[column]];
|
|
745
|
+
|
|
746
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
747
|
+
try {
|
|
748
|
+
fkValue = FieldTransformer.toDatabase(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
749
|
+
} catch(transformError) {
|
|
750
|
+
throw new Error(`UPDATE failed: ${transformError.message}`);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
try {
|
|
754
|
+
fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
755
|
+
} catch(typeError) {
|
|
756
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
757
|
+
}
|
|
758
|
+
var fore = `_${dirtyFields[column]}`;
|
|
759
|
+
sqlParts.push(`[${foreignKey}] = ?`);
|
|
760
|
+
params.push(model[fore]);
|
|
761
|
+
break;
|
|
762
|
+
case "integer":
|
|
763
|
+
var intValue = model["_" + dirtyFields[column]];
|
|
764
|
+
|
|
765
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
766
|
+
try {
|
|
767
|
+
intValue = FieldTransformer.toDatabase(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
768
|
+
} catch(transformError) {
|
|
769
|
+
throw new Error(`UPDATE failed: ${transformError.message}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
try {
|
|
773
|
+
intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
774
|
+
} catch(typeError) {
|
|
775
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
776
|
+
}
|
|
777
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
778
|
+
params.push(intValue);
|
|
779
|
+
break;
|
|
780
|
+
case "string":
|
|
781
|
+
var strValue = model[dirtyFields[column]];
|
|
782
|
+
|
|
783
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
784
|
+
try {
|
|
785
|
+
strValue = FieldTransformer.toDatabase(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
786
|
+
} catch(transformError) {
|
|
787
|
+
throw new Error(`UPDATE failed: ${transformError.message}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
792
|
+
} catch(typeError) {
|
|
793
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
794
|
+
}
|
|
795
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
796
|
+
params.push(strValue);
|
|
797
|
+
break;
|
|
798
|
+
case "boolean":
|
|
799
|
+
var boolValue = model[dirtyFields[column]];
|
|
800
|
+
|
|
801
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
802
|
+
try {
|
|
803
|
+
boolValue = FieldTransformer.toDatabase(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
804
|
+
} catch(transformError) {
|
|
805
|
+
throw new Error(`UPDATE failed: ${transformError.message}`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
810
|
+
} catch(typeError) {
|
|
811
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Convert to database-specific format (e.g., boolean → 1/0 for SQLite)
|
|
815
|
+
boolValue = $that._convertValueForDatabase(boolValue, model.__entity[dirtyFields[column]].type);
|
|
816
|
+
|
|
817
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
818
|
+
params.push(boolValue);
|
|
819
|
+
break;
|
|
820
|
+
case "time":
|
|
821
|
+
var timeValue = model[dirtyFields[column]];
|
|
822
|
+
|
|
823
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
824
|
+
try {
|
|
825
|
+
timeValue = FieldTransformer.toDatabase(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
826
|
+
} catch(transformError) {
|
|
827
|
+
throw new Error(`UPDATE failed: ${transformError.message}`);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
832
|
+
} catch(typeError) {
|
|
833
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
834
|
+
}
|
|
835
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
836
|
+
params.push(timeValue);
|
|
837
|
+
break;
|
|
838
|
+
case "hasMany":
|
|
839
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
840
|
+
params.push(model[dirtyFields[column]]);
|
|
841
|
+
break;
|
|
842
|
+
default:
|
|
843
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
844
|
+
params.push(model[dirtyFields[column]]);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if(sqlParts.length > 0){
|
|
849
|
+
return {
|
|
850
|
+
sql: sqlParts.join(', '),
|
|
851
|
+
params: params
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
else{
|
|
855
|
+
return -1;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
|
|
655
860
|
_buildDeleteObject(currentModel){
|
|
656
861
|
var primaryKey = currentModel.__Key === undefined ? tools.getPrimaryKeyObject(currentModel.__entity) : currentModel.__Key;
|
|
657
862
|
var value = currentModel.__value === undefined ? currentModel[primaryKey] : currentModel.__value;
|
|
@@ -713,7 +918,7 @@ class SQLLiteEngine {
|
|
|
713
918
|
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected string, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
714
919
|
|
|
715
920
|
case "boolean":
|
|
716
|
-
// Coerce to boolean
|
|
921
|
+
// Coerce to boolean (then convert for database)
|
|
717
922
|
if(actualType === 'boolean'){
|
|
718
923
|
return value;
|
|
719
924
|
}
|
|
@@ -751,6 +956,27 @@ class SQLLiteEngine {
|
|
|
751
956
|
}
|
|
752
957
|
}
|
|
753
958
|
|
|
959
|
+
/**
|
|
960
|
+
* Convert validated value to database-specific format
|
|
961
|
+
* Modern ORM pattern: transparent database-specific conversions
|
|
962
|
+
*
|
|
963
|
+
* @param {*} value - Already validated value
|
|
964
|
+
* @param {string} fieldType - Field type from entity definition
|
|
965
|
+
* @returns {*} Database-ready value
|
|
966
|
+
*/
|
|
967
|
+
_convertValueForDatabase(value, fieldType){
|
|
968
|
+
if(value === undefined || value === null){
|
|
969
|
+
return value;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// SQLite boolean conversion: JavaScript boolean → INTEGER (1/0)
|
|
973
|
+
if(fieldType === 'boolean' && typeof value === 'boolean'){
|
|
974
|
+
return value ? 1 : 0;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return value;
|
|
978
|
+
}
|
|
979
|
+
|
|
754
980
|
|
|
755
981
|
// return columns and value strings
|
|
756
982
|
_buildSQLInsertObject(fields, modelEntity){
|
|
@@ -817,6 +1043,79 @@ class SQLLiteEngine {
|
|
|
817
1043
|
|
|
818
1044
|
}
|
|
819
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* NEW SECURE VERSION: Build SQL INSERT with parameterized queries
|
|
1048
|
+
* Returns {tableName, columns, placeholders, params}
|
|
1049
|
+
* This prevents SQL injection by separating SQL structure from values
|
|
1050
|
+
*/
|
|
1051
|
+
_buildSQLInsertObjectParameterized(fields, modelEntity){
|
|
1052
|
+
var $that = this;
|
|
1053
|
+
var columnNames = [];
|
|
1054
|
+
var params = [];
|
|
1055
|
+
|
|
1056
|
+
for (var column in modelEntity) {
|
|
1057
|
+
// Skip internal properties
|
|
1058
|
+
if(column.indexOf("__") === -1 ){
|
|
1059
|
+
var fieldColumn = fields[column];
|
|
1060
|
+
|
|
1061
|
+
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
1062
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
1063
|
+
try {
|
|
1064
|
+
fieldColumn = FieldTransformer.toDatabase(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
1065
|
+
} catch(transformError) {
|
|
1066
|
+
throw new Error(`INSERT failed: ${transformError.message}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// Validate and coerce field type before processing
|
|
1070
|
+
try {
|
|
1071
|
+
fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
1072
|
+
} catch(typeError) {
|
|
1073
|
+
throw new Error(`INSERT failed: ${typeError.message}`);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Convert to database-specific format (e.g., boolean → 1/0 for SQLite)
|
|
1077
|
+
fieldColumn = $that._convertValueForDatabase(fieldColumn, modelEntity[column].type);
|
|
1078
|
+
|
|
1079
|
+
var relationship = modelEntity[column].relationshipType;
|
|
1080
|
+
var actualColumn = relationship === "belongsTo" ? modelEntity[column].foreignKey : column;
|
|
1081
|
+
|
|
1082
|
+
// Add column name and parameter
|
|
1083
|
+
columnNames.push(`[${actualColumn}]`);
|
|
1084
|
+
params.push(fieldColumn);
|
|
1085
|
+
}
|
|
1086
|
+
else{
|
|
1087
|
+
switch(modelEntity[column].type){
|
|
1088
|
+
case "belongsTo":
|
|
1089
|
+
var fieldObject = tools.findTrackedObject(fields.__context.__trackedEntities, column);
|
|
1090
|
+
if(Object.keys(fieldObject).length > 0){
|
|
1091
|
+
var primaryKey = tools.getPrimaryKeyObject(fieldObject.__entity);
|
|
1092
|
+
fieldColumn = fieldObject[primaryKey];
|
|
1093
|
+
var actualColumn = modelEntity[column].foreignKey;
|
|
1094
|
+
columnNames.push(`[${actualColumn}]`);
|
|
1095
|
+
params.push(fieldColumn);
|
|
1096
|
+
} else{
|
|
1097
|
+
console.log("Cannot find belongs to relationship")
|
|
1098
|
+
}
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if(columnNames.length > 0){
|
|
1106
|
+
// Create placeholders: ?, ?, ?, ...
|
|
1107
|
+
var placeholders = params.map(() => '?').join(', ');
|
|
1108
|
+
return {
|
|
1109
|
+
tableName: modelEntity.__name,
|
|
1110
|
+
columns: columnNames.join(', '),
|
|
1111
|
+
placeholders: placeholders,
|
|
1112
|
+
params: params
|
|
1113
|
+
};
|
|
1114
|
+
} else {
|
|
1115
|
+
return -1;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
820
1119
|
// will add double single quotes to allow string to be saved.
|
|
821
1120
|
_santizeSingleQuotes(value, context){
|
|
822
1121
|
if (typeof value === 'string' || value instanceof String){
|
|
@@ -854,11 +1153,23 @@ class SQLLiteEngine {
|
|
|
854
1153
|
return this.db.exec(query);
|
|
855
1154
|
}
|
|
856
1155
|
|
|
1156
|
+
_executeWithParams(query, params = []){
|
|
1157
|
+
console.log("SQL:", query);
|
|
1158
|
+
console.log("Params:", params);
|
|
1159
|
+
return this.db.prepare(query).run(...params);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
857
1162
|
_run(query){
|
|
858
1163
|
console.log("SQL:", query);
|
|
859
1164
|
return this.db.prepare(query).run();
|
|
860
1165
|
}
|
|
861
1166
|
|
|
1167
|
+
_runWithParams(query, params = []){
|
|
1168
|
+
console.log("SQL:", query);
|
|
1169
|
+
console.log("Params:", params);
|
|
1170
|
+
return this.db.prepare(query).run(...params);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
862
1173
|
setDB(db, type){
|
|
863
1174
|
this.db = db;
|
|
864
1175
|
this.dbType = type; // this will let us know which type of sqlengine to use.
|