masterrecord 0.2.34 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +25 -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 +14 -5
- package/SQLLiteEngine.js +309 -19
- package/context.js +57 -12
- 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 +249 -17
- package/package.json +6 -6
- package/postgresEngine.js +434 -491
- package/postgresSyncConnect.js +209 -0
- package/readme.md +1121 -265
- 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/tablePrefixTest.js +100 -0
- package/test/transformerTest.js +287 -0
- package/test/verifyFindById.js +169 -0
- package/test/verifyNewMethod.js +191 -0
- package/test/whereChainingTest.js +88 -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
|
|
|
@@ -140,7 +149,7 @@ class queryScript{
|
|
|
140
149
|
else if(type === "where"){
|
|
141
150
|
// If where already exists, merge new expressions into existing where so multiple
|
|
142
151
|
// chained where(...) calls combine into a single WHERE clause (joined by AND).
|
|
143
|
-
if(obj.where && obj[entityName] && cachedExpr[entityName]){
|
|
152
|
+
if(obj.where && obj.where[entityName] && cachedExpr[entityName]){
|
|
144
153
|
const existingQuery = obj.where[entityName].query || {};
|
|
145
154
|
const incomingQuery = cachedExpr[entityName].query || {};
|
|
146
155
|
const existingExprs = existingQuery.expressions || [];
|
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,172 @@ 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
|
+
var bool;
|
|
814
|
+
if(model.__entity[dirtyFields[column]].valueConversion){
|
|
815
|
+
bool = tools.convertBooleanToNumber(boolValue);
|
|
816
|
+
}
|
|
817
|
+
else{
|
|
818
|
+
bool = boolValue;
|
|
819
|
+
}
|
|
820
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
821
|
+
params.push(bool);
|
|
822
|
+
break;
|
|
823
|
+
case "time":
|
|
824
|
+
var timeValue = model[dirtyFields[column]];
|
|
825
|
+
|
|
826
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
827
|
+
try {
|
|
828
|
+
timeValue = FieldTransformer.toDatabase(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
829
|
+
} catch(transformError) {
|
|
830
|
+
throw new Error(`UPDATE failed: ${transformError.message}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
835
|
+
} catch(typeError) {
|
|
836
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
837
|
+
}
|
|
838
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
839
|
+
params.push(timeValue);
|
|
840
|
+
break;
|
|
841
|
+
case "hasMany":
|
|
842
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
843
|
+
params.push(model[dirtyFields[column]]);
|
|
844
|
+
break;
|
|
845
|
+
default:
|
|
846
|
+
sqlParts.push(`[${dirtyFields[column]}] = ?`);
|
|
847
|
+
params.push(model[dirtyFields[column]]);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if(sqlParts.length > 0){
|
|
852
|
+
return {
|
|
853
|
+
sql: sqlParts.join(', '),
|
|
854
|
+
params: params
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
else{
|
|
858
|
+
return -1;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
|
|
655
863
|
_buildDeleteObject(currentModel){
|
|
656
864
|
var primaryKey = currentModel.__Key === undefined ? tools.getPrimaryKeyObject(currentModel.__entity) : currentModel.__Key;
|
|
657
865
|
var value = currentModel.__value === undefined ? currentModel[primaryKey] : currentModel.__value;
|
|
@@ -817,6 +1025,76 @@ class SQLLiteEngine {
|
|
|
817
1025
|
|
|
818
1026
|
}
|
|
819
1027
|
|
|
1028
|
+
/**
|
|
1029
|
+
* NEW SECURE VERSION: Build SQL INSERT with parameterized queries
|
|
1030
|
+
* Returns {tableName, columns, placeholders, params}
|
|
1031
|
+
* This prevents SQL injection by separating SQL structure from values
|
|
1032
|
+
*/
|
|
1033
|
+
_buildSQLInsertObjectParameterized(fields, modelEntity){
|
|
1034
|
+
var $that = this;
|
|
1035
|
+
var columnNames = [];
|
|
1036
|
+
var params = [];
|
|
1037
|
+
|
|
1038
|
+
for (var column in modelEntity) {
|
|
1039
|
+
// Skip internal properties
|
|
1040
|
+
if(column.indexOf("__") === -1 ){
|
|
1041
|
+
var fieldColumn = fields[column];
|
|
1042
|
+
|
|
1043
|
+
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
1044
|
+
// 🔥 Apply toDatabase transformer before validation
|
|
1045
|
+
try {
|
|
1046
|
+
fieldColumn = FieldTransformer.toDatabase(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
1047
|
+
} catch(transformError) {
|
|
1048
|
+
throw new Error(`INSERT failed: ${transformError.message}`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Validate and coerce field type before processing
|
|
1052
|
+
try {
|
|
1053
|
+
fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
1054
|
+
} catch(typeError) {
|
|
1055
|
+
throw new Error(`INSERT failed: ${typeError.message}`);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
var relationship = modelEntity[column].relationshipType;
|
|
1059
|
+
var actualColumn = relationship === "belongsTo" ? modelEntity[column].foreignKey : column;
|
|
1060
|
+
|
|
1061
|
+
// Add column name and parameter
|
|
1062
|
+
columnNames.push(`[${actualColumn}]`);
|
|
1063
|
+
params.push(fieldColumn);
|
|
1064
|
+
}
|
|
1065
|
+
else{
|
|
1066
|
+
switch(modelEntity[column].type){
|
|
1067
|
+
case "belongsTo":
|
|
1068
|
+
var fieldObject = tools.findTrackedObject(fields.__context.__trackedEntities, column);
|
|
1069
|
+
if(Object.keys(fieldObject).length > 0){
|
|
1070
|
+
var primaryKey = tools.getPrimaryKeyObject(fieldObject.__entity);
|
|
1071
|
+
fieldColumn = fieldObject[primaryKey];
|
|
1072
|
+
var actualColumn = modelEntity[column].foreignKey;
|
|
1073
|
+
columnNames.push(`[${actualColumn}]`);
|
|
1074
|
+
params.push(fieldColumn);
|
|
1075
|
+
} else{
|
|
1076
|
+
console.log("Cannot find belongs to relationship")
|
|
1077
|
+
}
|
|
1078
|
+
break;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if(columnNames.length > 0){
|
|
1085
|
+
// Create placeholders: ?, ?, ?, ...
|
|
1086
|
+
var placeholders = params.map(() => '?').join(', ');
|
|
1087
|
+
return {
|
|
1088
|
+
tableName: modelEntity.__name,
|
|
1089
|
+
columns: columnNames.join(', '),
|
|
1090
|
+
placeholders: placeholders,
|
|
1091
|
+
params: params
|
|
1092
|
+
};
|
|
1093
|
+
} else {
|
|
1094
|
+
return -1;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
820
1098
|
// will add double single quotes to allow string to be saved.
|
|
821
1099
|
_santizeSingleQuotes(value, context){
|
|
822
1100
|
if (typeof value === 'string' || value instanceof String){
|
|
@@ -854,11 +1132,23 @@ class SQLLiteEngine {
|
|
|
854
1132
|
return this.db.exec(query);
|
|
855
1133
|
}
|
|
856
1134
|
|
|
1135
|
+
_executeWithParams(query, params = []){
|
|
1136
|
+
console.log("SQL:", query);
|
|
1137
|
+
console.log("Params:", params);
|
|
1138
|
+
return this.db.prepare(query).run(...params);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
857
1141
|
_run(query){
|
|
858
1142
|
console.log("SQL:", query);
|
|
859
1143
|
return this.db.prepare(query).run();
|
|
860
1144
|
}
|
|
861
1145
|
|
|
1146
|
+
_runWithParams(query, params = []){
|
|
1147
|
+
console.log("SQL:", query);
|
|
1148
|
+
console.log("Params:", params);
|
|
1149
|
+
return this.db.prepare(query).run(...params);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
862
1152
|
setDB(db, type){
|
|
863
1153
|
this.db = db;
|
|
864
1154
|
this.dbType = type; // this will let us know which type of sqlengine to use.
|
package/context.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// Version 0.0.
|
|
1
|
+
// Version 0.0.17
|
|
2
2
|
|
|
3
3
|
var modelBuilder = require('./Entity/entityModelBuilder');
|
|
4
4
|
var query = require('masterrecord/QueryLanguage/queryMethods');
|
|
5
5
|
var tools = require('./Tools');
|
|
6
6
|
var SQLLiteEngine = require('masterrecord/SQLLiteEngine');
|
|
7
7
|
var MYSQLEngine = require('masterrecord/mySQLEngine');
|
|
8
|
+
var PostgresEngine = require('masterrecord/postgresEngine');
|
|
8
9
|
var insertManager = require('./insertManager');
|
|
9
10
|
var deleteManager = require('./deleteManager');
|
|
10
11
|
var globSearch = require("glob");
|
|
@@ -12,6 +13,7 @@ var fs = require('fs');
|
|
|
12
13
|
var path = require('path');
|
|
13
14
|
const appRoot = require('app-root-path');
|
|
14
15
|
const MySQLClient = require('masterrecord/mySQLSyncConnect');
|
|
16
|
+
const PostgresClient = require('masterrecord/postgresSyncConnect');
|
|
15
17
|
|
|
16
18
|
class context {
|
|
17
19
|
_isModelValid = {
|
|
@@ -24,6 +26,7 @@ class context {
|
|
|
24
26
|
__relationshipModels = [];
|
|
25
27
|
__environment = "";
|
|
26
28
|
__name = "";
|
|
29
|
+
tablePrefix = "";
|
|
27
30
|
isSQLite = false;
|
|
28
31
|
isMySQL = false;
|
|
29
32
|
isPostgres = false;
|
|
@@ -71,7 +74,7 @@ class context {
|
|
|
71
74
|
*/
|
|
72
75
|
__mysqlInit(env, sqlName){
|
|
73
76
|
try{
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
//const mysql = require(sqlName);
|
|
76
79
|
const connection = new MySQLClient(env);
|
|
77
80
|
this._SQLEngine = new MYSQLEngine();
|
|
@@ -84,6 +87,31 @@ class context {
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/*
|
|
91
|
+
postgres expected model
|
|
92
|
+
{
|
|
93
|
+
"type": "postgres",
|
|
94
|
+
host : 'localhost',
|
|
95
|
+
port : 5432,
|
|
96
|
+
user : 'me',
|
|
97
|
+
password : 'secret',
|
|
98
|
+
database : 'my_db'
|
|
99
|
+
}
|
|
100
|
+
*/
|
|
101
|
+
async __postgresInit(env, sqlName){
|
|
102
|
+
try{
|
|
103
|
+
const connection = new PostgresClient();
|
|
104
|
+
await connection.connect(env);
|
|
105
|
+
this._SQLEngine = connection.getEngine();
|
|
106
|
+
this._SQLEngine.__name = sqlName;
|
|
107
|
+
return connection.getPool();
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
console.log("error PostgreSQL", e);
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
87
115
|
__clearErrorHandler(){
|
|
88
116
|
this._isModelValid = {
|
|
89
117
|
isValid: true,
|
|
@@ -211,13 +239,23 @@ class context {
|
|
|
211
239
|
}
|
|
212
240
|
|
|
213
241
|
if(type === 'mysql'){
|
|
214
|
-
this.isMySQL = true; this.isSQLite = false;
|
|
242
|
+
this.isMySQL = true; this.isSQLite = false; this.isPostgres = false;
|
|
215
243
|
this.db = this.__mysqlInit(options, 'mysql2');
|
|
216
244
|
this._SQLEngine.setDB(this.db, 'mysql');
|
|
217
245
|
return this;
|
|
218
246
|
}
|
|
219
247
|
|
|
220
|
-
|
|
248
|
+
if(type === 'postgres' || type === 'postgresql'){
|
|
249
|
+
this.isPostgres = true; this.isMySQL = false; this.isSQLite = false;
|
|
250
|
+
// Postgres is async, so we need to handle promises
|
|
251
|
+
(async () => {
|
|
252
|
+
this.db = await this.__postgresInit(options, 'pg');
|
|
253
|
+
// Note: engine is already set in __postgresInit
|
|
254
|
+
})();
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(`Unsupported database type '${options.type}'. Expected 'sqlite', 'mysql', or 'postgres'.`);
|
|
221
259
|
}
|
|
222
260
|
catch(err){
|
|
223
261
|
console.log("error:", err);
|
|
@@ -348,7 +386,14 @@ class context {
|
|
|
348
386
|
|
|
349
387
|
dbset(model, name){
|
|
350
388
|
var validModel = modelBuilder.create(model);
|
|
351
|
-
|
|
389
|
+
var tableName = name === undefined ? model.name : name;
|
|
390
|
+
|
|
391
|
+
// Apply tablePrefix if set
|
|
392
|
+
if(this.tablePrefix && typeof this.tablePrefix === 'string' && this.tablePrefix.length > 0){
|
|
393
|
+
tableName = this.tablePrefix + tableName;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
validModel.__name = tableName;
|
|
352
397
|
this.__entities.push(validModel); // model object
|
|
353
398
|
var buildMod = tools.createNewInstance(validModel, query, this);
|
|
354
399
|
this.__builderEntities.push(buildMod); // query builder entites
|
|
@@ -377,9 +422,9 @@ class context {
|
|
|
377
422
|
break;
|
|
378
423
|
case "modified":
|
|
379
424
|
if(currentModel.__dirtyFields.length > 0){
|
|
380
|
-
var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
|
|
381
|
-
//
|
|
382
|
-
var argu = this._SQLEngine.
|
|
425
|
+
var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
|
|
426
|
+
// Use NEW SECURE parameterized version
|
|
427
|
+
var argu = this._SQLEngine._buildSQLEqualToParameterized(cleanCurrentModel);
|
|
383
428
|
if(argu !== -1 ){
|
|
384
429
|
var primaryKey = tools.getPrimaryKeyObject(cleanCurrentModel.__entity);
|
|
385
430
|
var sqlUpdate = {tableName: cleanCurrentModel.__entity.__name, arg: argu, primaryKey : primaryKey, primaryKeyValue : cleanCurrentModel[primaryKey] };
|
|
@@ -388,7 +433,7 @@ class context {
|
|
|
388
433
|
else{
|
|
389
434
|
console.log("Nothing has been tracked, modified, created or added");
|
|
390
435
|
}
|
|
391
|
-
|
|
436
|
+
|
|
392
437
|
}
|
|
393
438
|
else{
|
|
394
439
|
console.log("Tracked entity modified with no values being changed");
|
|
@@ -419,8 +464,8 @@ class context {
|
|
|
419
464
|
case "modified":
|
|
420
465
|
if(currentModel.__dirtyFields.length > 0){
|
|
421
466
|
var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
|
|
422
|
-
//
|
|
423
|
-
var argu = this._SQLEngine.
|
|
467
|
+
// Use NEW SECURE parameterized version
|
|
468
|
+
var argu = this._SQLEngine._buildSQLEqualToParameterized(cleanCurrentModel);
|
|
424
469
|
if(argu !== -1 ){
|
|
425
470
|
var primaryKey = tools.getPrimaryKeyObject(cleanCurrentModel.__entity);
|
|
426
471
|
var sqlUpdate = {tableName: cleanCurrentModel.__entity.__name, arg: argu, primaryKey : primaryKey, primaryKeyValue : cleanCurrentModel[primaryKey] };
|
|
@@ -429,7 +474,7 @@ class context {
|
|
|
429
474
|
else{
|
|
430
475
|
console.log("Nothing has been tracked, modified, created or added");
|
|
431
476
|
}
|
|
432
|
-
|
|
477
|
+
|
|
433
478
|
}
|
|
434
479
|
else{
|
|
435
480
|
console.log("Tracked entity modified with no values being changed");
|