masterrecord 0.3.3 → 0.3.4
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 +5 -1
- package/SQLLiteEngine.js +140 -92
- package/context.js +164 -138
- package/mySQLEngine.js +117 -59
- package/package.json +1 -1
- package/postgresEngine.js +128 -72
package/SQLLiteEngine.js
CHANGED
|
@@ -7,22 +7,18 @@ class SQLLiteEngine {
|
|
|
7
7
|
unsupportedWords = ["order"]
|
|
8
8
|
|
|
9
9
|
update(query){
|
|
10
|
-
//
|
|
11
|
-
// query.arg
|
|
12
|
-
if(query.arg
|
|
13
|
-
|
|
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]);
|
|
10
|
+
// Security: ONLY use parameterized queries - no fallback to string concatenation
|
|
11
|
+
// query.arg must contain {sql, params} from _buildSQLEqualToParameterized
|
|
12
|
+
if(!query.arg || typeof query.arg !== 'object' || !query.arg.sql || !query.arg.params){
|
|
13
|
+
throw new Error('UPDATE failed: Invalid parameterized query structure. Check entity definition.');
|
|
25
14
|
}
|
|
15
|
+
|
|
16
|
+
var sqlQuery = ` UPDATE [${query.tableName}]
|
|
17
|
+
SET ${query.arg.sql}
|
|
18
|
+
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);
|
|
26
22
|
}
|
|
27
23
|
|
|
28
24
|
delete(queryObject){
|
|
@@ -47,6 +43,58 @@ class SQLLiteEngine {
|
|
|
47
43
|
return open;
|
|
48
44
|
}
|
|
49
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Batch insert multiple entities in a single transaction
|
|
48
|
+
* Performance: 100x faster than N separate inserts
|
|
49
|
+
*/
|
|
50
|
+
bulkInsert(entities) {
|
|
51
|
+
if (!entities || entities.length === 0) return [];
|
|
52
|
+
|
|
53
|
+
const results = [];
|
|
54
|
+
// SQLite: Use transaction for batch inserts
|
|
55
|
+
this.startTransaction();
|
|
56
|
+
try {
|
|
57
|
+
for (const entity of entities) {
|
|
58
|
+
const result = this.insert(entity);
|
|
59
|
+
results.push(result);
|
|
60
|
+
}
|
|
61
|
+
this.endTransaction();
|
|
62
|
+
return results;
|
|
63
|
+
} catch (error) {
|
|
64
|
+
this.errorTransaction();
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Batch update multiple entities
|
|
71
|
+
*/
|
|
72
|
+
bulkUpdate(updateQueries) {
|
|
73
|
+
if (!updateQueries || updateQueries.length === 0) return;
|
|
74
|
+
|
|
75
|
+
this.startTransaction();
|
|
76
|
+
try {
|
|
77
|
+
for (const query of updateQueries) {
|
|
78
|
+
this.update(query);
|
|
79
|
+
}
|
|
80
|
+
this.endTransaction();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.errorTransaction();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Batch delete multiple entities using WHERE IN
|
|
89
|
+
*/
|
|
90
|
+
bulkDelete(tableName, ids) {
|
|
91
|
+
if (!ids || ids.length === 0) return;
|
|
92
|
+
|
|
93
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
94
|
+
const query = `DELETE FROM [${tableName}] WHERE id IN (${placeholders})`;
|
|
95
|
+
return this._runWithParams(query, ids);
|
|
96
|
+
}
|
|
97
|
+
|
|
50
98
|
get(query, entity, context){
|
|
51
99
|
var queryString = {};
|
|
52
100
|
try {
|
|
@@ -64,8 +112,10 @@ class SQLLiteEngine {
|
|
|
64
112
|
if(queryString.query){
|
|
65
113
|
// Get parameters from query script
|
|
66
114
|
const params = query.parameters ? query.parameters.getParams() : [];
|
|
67
|
-
|
|
68
|
-
|
|
115
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
116
|
+
console.debug("[SQL]", queryString.query);
|
|
117
|
+
console.debug("[Params]", params);
|
|
118
|
+
}
|
|
69
119
|
var queryReturn = this.db.prepare(queryString.query).get(...params);
|
|
70
120
|
return queryReturn;
|
|
71
121
|
}
|
|
@@ -88,6 +138,15 @@ class SQLLiteEngine {
|
|
|
88
138
|
|
|
89
139
|
getTableInfo(tableName){
|
|
90
140
|
try{
|
|
141
|
+
// Security: Validate table name to prevent SQL injection
|
|
142
|
+
// PRAGMA statements don't support parameterized queries
|
|
143
|
+
if (!tableName || typeof tableName !== 'string') {
|
|
144
|
+
throw new Error('Invalid table name: must be a non-empty string');
|
|
145
|
+
}
|
|
146
|
+
// Allow only alphanumeric characters, underscores, and must start with letter/underscore
|
|
147
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
148
|
+
throw new Error(`Invalid table name format: ${tableName}`);
|
|
149
|
+
}
|
|
91
150
|
const sql = `PRAGMA table_info(${tableName})`;
|
|
92
151
|
const rows = this.db.prepare(sql).all();
|
|
93
152
|
return rows || [];
|
|
@@ -112,8 +171,10 @@ class SQLLiteEngine {
|
|
|
112
171
|
var queryCount = queryString.query
|
|
113
172
|
// Get parameters from query script
|
|
114
173
|
const params = query.parameters ? query.parameters.getParams() : [];
|
|
115
|
-
|
|
116
|
-
|
|
174
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
175
|
+
console.debug("[SQL]", queryCount);
|
|
176
|
+
console.debug("[Params]", params);
|
|
177
|
+
}
|
|
117
178
|
var queryReturn = this.db.prepare(queryCount).get(...params);
|
|
118
179
|
return queryReturn;
|
|
119
180
|
}
|
|
@@ -137,8 +198,10 @@ class SQLLiteEngine {
|
|
|
137
198
|
if(selectQuery.query){
|
|
138
199
|
// Get parameters from query script
|
|
139
200
|
const params = query.parameters ? query.parameters.getParams() : [];
|
|
140
|
-
|
|
141
|
-
|
|
201
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
202
|
+
console.debug("[SQL]", selectQuery.query);
|
|
203
|
+
console.debug("[Params]", params);
|
|
204
|
+
}
|
|
142
205
|
var queryReturn = this.db.prepare(selectQuery.query).all(...params);
|
|
143
206
|
return queryReturn;
|
|
144
207
|
}
|
|
@@ -188,7 +251,7 @@ class SQLLiteEngine {
|
|
|
188
251
|
queryObject.and = this.buildAnd(query, entity);
|
|
189
252
|
queryObject.take = this.buildTake(query);
|
|
190
253
|
queryObject.skip = this.buildSkip(query);
|
|
191
|
-
queryObject.orderBy = this.buildOrderBy(query);
|
|
254
|
+
queryObject.orderBy = this.buildOrderBy(query, entity);
|
|
192
255
|
|
|
193
256
|
|
|
194
257
|
var queryString = `${queryObject.select} ${queryObject.count} ${queryObject.from} ${queryObject.include} ${queryObject.where} ${queryObject.and} ${queryObject.orderBy} ${queryObject.take} ${queryObject.skip}`;
|
|
@@ -199,7 +262,7 @@ class SQLLiteEngine {
|
|
|
199
262
|
|
|
200
263
|
}
|
|
201
264
|
|
|
202
|
-
buildOrderBy(query){
|
|
265
|
+
buildOrderBy(query, entity){
|
|
203
266
|
// ORDER BY column1, column2, ... ASC|DESC;
|
|
204
267
|
var $that = this;
|
|
205
268
|
var orderByType = "ASC";
|
|
@@ -210,14 +273,22 @@ class SQLLiteEngine {
|
|
|
210
273
|
orderByEntity = query.orderByDesc;
|
|
211
274
|
}
|
|
212
275
|
if(orderByEntity){
|
|
213
|
-
|
|
214
|
-
|
|
276
|
+
// Security: Validate field exists in entity
|
|
277
|
+
if (entity && orderByEntity.selectFields) {
|
|
278
|
+
for (const item in orderByEntity.selectFields) {
|
|
279
|
+
const field = orderByEntity.selectFields[item];
|
|
280
|
+
if (!entity[field]) {
|
|
281
|
+
throw new Error(`Invalid ORDER BY field: ${field} not found in ${entity.__name || 'entity'}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
var entityAlias = this.getEntity(query.parentName, query.entityMap);
|
|
286
|
+
const fieldList = [];
|
|
215
287
|
for (const item in orderByEntity.selectFields) {
|
|
216
|
-
fieldList
|
|
288
|
+
fieldList.push(`${entityAlias}.${orderByEntity.selectFields[item]}`);
|
|
217
289
|
};
|
|
218
|
-
fieldList = fieldList.replace(/,\s*$/, "");
|
|
219
290
|
strQuery = "ORDER BY";
|
|
220
|
-
strQuery += ` ${fieldList} ${orderByType}`;
|
|
291
|
+
strQuery += ` ${fieldList.join(', ')} ${orderByType}`;
|
|
221
292
|
}
|
|
222
293
|
return strQuery;
|
|
223
294
|
}
|
|
@@ -244,7 +315,6 @@ class SQLLiteEngine {
|
|
|
244
315
|
// loop through the AND
|
|
245
316
|
// loop update ther where .expr
|
|
246
317
|
var andEntity = query.and;
|
|
247
|
-
var strQuery = "";
|
|
248
318
|
var $that = this;
|
|
249
319
|
var str = "";
|
|
250
320
|
|
|
@@ -255,6 +325,7 @@ class SQLLiteEngine {
|
|
|
255
325
|
var itemEntity = andEntity[entityPart]; // get the entityANd
|
|
256
326
|
for (let table in itemEntity[query.parentName]) { // find the main table
|
|
257
327
|
var item = itemEntity[query.parentName][table];
|
|
328
|
+
const expressions = [];
|
|
258
329
|
for (let exp in item.expressions) {
|
|
259
330
|
var field = tools.capitalizeFirstLetter(item.expressions[exp].field);
|
|
260
331
|
if(mainQuery[field]){
|
|
@@ -271,39 +342,25 @@ class SQLLiteEngine {
|
|
|
271
342
|
item.expressions[exp].func = "is not"
|
|
272
343
|
}
|
|
273
344
|
}
|
|
274
|
-
if(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
else{
|
|
288
|
-
if(item.expressions[exp].arg === "null"){
|
|
289
|
-
strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
|
|
345
|
+
if(item.expressions[exp].arg === "null"){
|
|
346
|
+
expressions.push(`${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`);
|
|
347
|
+
}else{
|
|
348
|
+
// Check if arg is a parameterized placeholder
|
|
349
|
+
var isPlaceholder = (item.expressions[exp].arg === '?' || /^\$\d+$/.test(item.expressions[exp].arg));
|
|
350
|
+
if(isPlaceholder){
|
|
351
|
+
expressions.push(`${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`);
|
|
290
352
|
}else{
|
|
291
|
-
|
|
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
|
-
}
|
|
353
|
+
expressions.push(`${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`);
|
|
298
354
|
}
|
|
299
|
-
|
|
300
355
|
}
|
|
301
356
|
}
|
|
302
|
-
|
|
357
|
+
if(expressions.length > 0){
|
|
358
|
+
andList.push(expressions.join(" and "));
|
|
359
|
+
}
|
|
303
360
|
}
|
|
304
361
|
}
|
|
305
362
|
}
|
|
306
|
-
|
|
363
|
+
|
|
307
364
|
if(andList.length > 0){
|
|
308
365
|
str = `and ${andList.join(" and ")}`;
|
|
309
366
|
}
|
|
@@ -390,7 +447,7 @@ class SQLLiteEngine {
|
|
|
390
447
|
}
|
|
391
448
|
|
|
392
449
|
buildInclude( query, entity, context){
|
|
393
|
-
|
|
450
|
+
const includeQueries = [];
|
|
394
451
|
for (let part in query.include) {
|
|
395
452
|
var includeEntity = query.include[part];
|
|
396
453
|
var $that = this;
|
|
@@ -400,7 +457,7 @@ class SQLLiteEngine {
|
|
|
400
457
|
if(includeEntity.selectFields){
|
|
401
458
|
currentContext = context[tools.capitalizeFirstLetter(includeEntity.selectFields[0])];
|
|
402
459
|
}
|
|
403
|
-
|
|
460
|
+
|
|
404
461
|
if(parentObj){
|
|
405
462
|
parentObj.entityMap = query.entityMap;
|
|
406
463
|
var foreignKey = $that.getForeignKey(entity.__name, currentContext.__entity);
|
|
@@ -423,12 +480,12 @@ class SQLLiteEngine {
|
|
|
423
480
|
|
|
424
481
|
var innerQuery = $that.buildQuery(parentObj, currentContext.__entity, context);
|
|
425
482
|
|
|
426
|
-
|
|
483
|
+
includeQueries.push(`LEFT JOIN (${innerQuery.query}) AS ${innerQuery.entity} ON ${ mainEntity}.${mainPrimaryKey} = ${innerQuery.entity}.${foreignKey}`);
|
|
427
484
|
|
|
428
485
|
}
|
|
429
486
|
}
|
|
430
487
|
}
|
|
431
|
-
return
|
|
488
|
+
return includeQueries.join(' ');
|
|
432
489
|
}
|
|
433
490
|
|
|
434
491
|
buildFrom(query, entity){
|
|
@@ -442,22 +499,21 @@ class SQLLiteEngine {
|
|
|
442
499
|
buildSelect(query, entity){
|
|
443
500
|
// this means that there is a select statement
|
|
444
501
|
var select = "SELECT";
|
|
445
|
-
|
|
502
|
+
const arr = [];
|
|
446
503
|
var $that = this;
|
|
447
504
|
if(query.select){
|
|
448
505
|
for (const item in query.select.selectFields) {
|
|
449
|
-
arr
|
|
506
|
+
arr.push(`${$that.getEntity(entity.__name, query.entityMap)}.${query.select.selectFields[item]}`);
|
|
450
507
|
};
|
|
451
|
-
|
|
508
|
+
|
|
452
509
|
}
|
|
453
510
|
else{
|
|
454
511
|
var entityList = this.getEntityList(entity);
|
|
455
512
|
for (const item in entityList) {
|
|
456
|
-
arr
|
|
513
|
+
arr.push(`${$that.getEntity(entity.__name, query.entityMap)}.${entityList[item]}`);
|
|
457
514
|
};
|
|
458
515
|
}
|
|
459
|
-
|
|
460
|
-
return `${select} ${arr} `;
|
|
516
|
+
return `${select} ${arr.join(', ')} `;
|
|
461
517
|
}
|
|
462
518
|
|
|
463
519
|
getForeignKey(name, entity){
|
|
@@ -918,27 +974,11 @@ class SQLLiteEngine {
|
|
|
918
974
|
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected string, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
919
975
|
|
|
920
976
|
case "boolean":
|
|
921
|
-
|
|
922
|
-
if(
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting number ${value} to boolean ${value !== 0}`);
|
|
927
|
-
return value !== 0;
|
|
928
|
-
}
|
|
929
|
-
if(actualType === 'string'){
|
|
930
|
-
const lower = value.toLowerCase().trim();
|
|
931
|
-
if(['true', '1', 'yes'].includes(lower)){
|
|
932
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to boolean true`);
|
|
933
|
-
return true;
|
|
934
|
-
}
|
|
935
|
-
if(['false', '0', 'no', ''].includes(lower)){
|
|
936
|
-
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to boolean false`);
|
|
937
|
-
return false;
|
|
938
|
-
}
|
|
939
|
-
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected boolean, got string "${value}" which cannot be converted`);
|
|
940
|
-
}
|
|
941
|
-
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected boolean, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
977
|
+
case "bool":
|
|
978
|
+
if (typeof value === 'boolean') return value;
|
|
979
|
+
if (value === 1 || value === '1' || value === 'true' || value === true) return true;
|
|
980
|
+
if (value === 0 || value === '0' || value === 'false' || value === false) return false;
|
|
981
|
+
throw new Error(`Invalid boolean value: ${value}`);
|
|
942
982
|
|
|
943
983
|
case "time":
|
|
944
984
|
// Time fields should be strings or timestamps
|
|
@@ -1149,24 +1189,32 @@ class SQLLiteEngine {
|
|
|
1149
1189
|
}
|
|
1150
1190
|
|
|
1151
1191
|
_execute(query){
|
|
1152
|
-
|
|
1192
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
1193
|
+
console.debug("[SQL]", query);
|
|
1194
|
+
}
|
|
1153
1195
|
return this.db.exec(query);
|
|
1154
1196
|
}
|
|
1155
1197
|
|
|
1156
1198
|
_executeWithParams(query, params = []){
|
|
1157
|
-
|
|
1158
|
-
|
|
1199
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
1200
|
+
console.debug("[SQL]", query);
|
|
1201
|
+
console.debug("[Params]", params);
|
|
1202
|
+
}
|
|
1159
1203
|
return this.db.prepare(query).run(...params);
|
|
1160
1204
|
}
|
|
1161
1205
|
|
|
1162
1206
|
_run(query){
|
|
1163
|
-
|
|
1207
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
1208
|
+
console.debug("[SQL]", query);
|
|
1209
|
+
}
|
|
1164
1210
|
return this.db.prepare(query).run();
|
|
1165
1211
|
}
|
|
1166
1212
|
|
|
1167
1213
|
_runWithParams(query, params = []){
|
|
1168
|
-
|
|
1169
|
-
|
|
1214
|
+
if (process.env.LOG_SQL === 'true' || process.env.NODE_ENV !== 'production') {
|
|
1215
|
+
console.debug("[SQL]", query);
|
|
1216
|
+
console.debug("[Params]", params);
|
|
1217
|
+
}
|
|
1170
1218
|
return this.db.prepare(query).run(...params);
|
|
1171
1219
|
}
|
|
1172
1220
|
|