masterrecord 0.2.36 → 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 +19 -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 +309 -19
- package/context.js +47 -10
- 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 +3 -3
- package/postgresEngine.js +434 -491
- 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
package/Migrations/schema.js
CHANGED
|
@@ -38,6 +38,14 @@ class schema{
|
|
|
38
38
|
var query = queryBuilder.addColum(table);
|
|
39
39
|
this.context._execute(query);
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
if(this.context.isPostgres){
|
|
43
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
44
|
+
var queryBuilder = new postgresQuery();
|
|
45
|
+
table.realDataType = queryBuilder.typeManager(table.type);
|
|
46
|
+
var query = queryBuilder.addColum(table);
|
|
47
|
+
this.context._execute(query);
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
// add column to database
|
|
@@ -46,7 +54,7 @@ class schema{
|
|
|
46
54
|
dropColumn(table){
|
|
47
55
|
if(table){
|
|
48
56
|
if(this.fullTable){
|
|
49
|
-
// drop column
|
|
57
|
+
// drop column
|
|
50
58
|
if(this.context.isSQLite){
|
|
51
59
|
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
52
60
|
var queryBuilder = new sqliteQuery();
|
|
@@ -61,6 +69,13 @@ class schema{
|
|
|
61
69
|
this.context._execute(query);
|
|
62
70
|
}
|
|
63
71
|
|
|
72
|
+
if(this.context.isPostgres){
|
|
73
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
74
|
+
var queryBuilder = new postgresQuery();
|
|
75
|
+
var query = queryBuilder.dropColumn(table);
|
|
76
|
+
this.context._execute(query);
|
|
77
|
+
}
|
|
78
|
+
|
|
64
79
|
}else{
|
|
65
80
|
console.log("Must call the addTable function.");
|
|
66
81
|
}
|
|
@@ -88,6 +103,13 @@ class schema{
|
|
|
88
103
|
var query = queryBuilder.createTable(table);
|
|
89
104
|
this.context._execute(query);
|
|
90
105
|
}
|
|
106
|
+
|
|
107
|
+
if(this.context.isPostgres){
|
|
108
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
109
|
+
var queryBuilder = new postgresQuery();
|
|
110
|
+
var query = queryBuilder.createTable(table);
|
|
111
|
+
this.context._execute(query);
|
|
112
|
+
}
|
|
91
113
|
}
|
|
92
114
|
}else{
|
|
93
115
|
console.log("Table that you're trying to create is undefined. Please check if there are any changes that need to be made");
|
|
@@ -130,6 +152,13 @@ class schema{
|
|
|
130
152
|
const query = queryBuilder.addColum(newCol);
|
|
131
153
|
this.context._execute(query);
|
|
132
154
|
}
|
|
155
|
+
if(this.context.isPostgres){
|
|
156
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
157
|
+
var queryBuilder = new postgresQuery();
|
|
158
|
+
newCol.realDataType = queryBuilder.typeManager(col.type);
|
|
159
|
+
const query = queryBuilder.addColum(newCol);
|
|
160
|
+
this.context._execute(query);
|
|
161
|
+
}
|
|
133
162
|
}
|
|
134
163
|
}
|
|
135
164
|
}
|
|
@@ -255,6 +284,13 @@ class schema{
|
|
|
255
284
|
var query = queryBuilder.dropTable(table.__name);
|
|
256
285
|
this.context._execute(query);
|
|
257
286
|
}
|
|
287
|
+
|
|
288
|
+
if(this.context.isPostgres){
|
|
289
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
290
|
+
var queryBuilder = new postgresQuery();
|
|
291
|
+
var query = queryBuilder.dropTable(table.__name);
|
|
292
|
+
this.context._execute(query);
|
|
293
|
+
}
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
|
|
@@ -272,10 +308,16 @@ class schema{
|
|
|
272
308
|
const admin = new MySQLClient(baseConfig);
|
|
273
309
|
admin.connect();
|
|
274
310
|
if(!admin.connection){ return; }
|
|
275
|
-
|
|
311
|
+
|
|
312
|
+
// Use parameterized query for checking database existence
|
|
313
|
+
const check = admin.query(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, [dbName]);
|
|
276
314
|
const exists = Array.isArray(check) ? check.length > 0 : !!check?.length;
|
|
277
315
|
if(!exists){
|
|
278
|
-
//
|
|
316
|
+
// Validate database name (alphanumeric, underscore, hyphen only)
|
|
317
|
+
if(!/^[a-zA-Z0-9_-]+$/.test(dbName)){
|
|
318
|
+
throw new Error(`Invalid database name: ${dbName}. Only alphanumeric characters, underscores, and hyphens are allowed.`);
|
|
319
|
+
}
|
|
320
|
+
// CREATE DATABASE doesn't support placeholders, but we've validated the name
|
|
279
321
|
admin.query(`CREATE DATABASE \`${dbName}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
280
322
|
}
|
|
281
323
|
admin.close();
|
|
@@ -313,6 +355,13 @@ class schema{
|
|
|
313
355
|
this.context._execute(query);
|
|
314
356
|
}
|
|
315
357
|
|
|
358
|
+
if(this.context.isPostgres){
|
|
359
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
360
|
+
var queryBuilder = new postgresQuery();
|
|
361
|
+
var query = queryBuilder.alterColumn(table);
|
|
362
|
+
this.context._execute(query);
|
|
363
|
+
}
|
|
364
|
+
|
|
316
365
|
}else{
|
|
317
366
|
console.log("Must call the addTable function.");
|
|
318
367
|
}
|
|
@@ -334,38 +383,92 @@ class schema{
|
|
|
334
383
|
var query = queryBuilder.renameColumn(table);
|
|
335
384
|
this.context._execute(query);
|
|
336
385
|
}
|
|
386
|
+
|
|
387
|
+
if(this.context.isPostgres){
|
|
388
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
389
|
+
var queryBuilder = new postgresQuery();
|
|
390
|
+
var query = queryBuilder.renameColumn(table);
|
|
391
|
+
this.context._execute(query);
|
|
392
|
+
}
|
|
337
393
|
}
|
|
338
394
|
}
|
|
339
395
|
|
|
340
396
|
seed(tableName, rows){
|
|
341
397
|
if(!tableName || !rows){ return; }
|
|
342
398
|
const items = Array.isArray(rows) ? rows : [rows];
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
399
|
+
|
|
400
|
+
// Use query builders for consistent seed data handling
|
|
401
|
+
if(this.context.isSQLite){
|
|
402
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
403
|
+
var queryBuilder = new sqliteQuery();
|
|
404
|
+
for(const row of items){
|
|
405
|
+
// SQLite: Use INSERT OR IGNORE for idempotency
|
|
406
|
+
const query = queryBuilder.insertSeedData(tableName, row);
|
|
407
|
+
const idempotentQuery = query.replace(/^INSERT INTO/, 'INSERT OR IGNORE INTO');
|
|
408
|
+
this.context._execute(idempotentQuery);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if(this.context.isMySQL){
|
|
413
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
414
|
+
var queryBuilder = new sqlquery();
|
|
415
|
+
for(const row of items){
|
|
416
|
+
// MySQL: Use INSERT IGNORE for idempotency
|
|
417
|
+
const query = queryBuilder.insertSeedData(tableName, row);
|
|
418
|
+
const idempotentQuery = query.replace(/^INSERT INTO/, 'INSERT IGNORE INTO');
|
|
419
|
+
this.context._execute(idempotentQuery);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if(this.context.isPostgres){
|
|
424
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
425
|
+
var queryBuilder = new postgresQuery();
|
|
426
|
+
for(const row of items){
|
|
427
|
+
// PostgreSQL: Use INSERT ... ON CONFLICT DO NOTHING for idempotency
|
|
428
|
+
// Note: This requires a unique constraint or primary key
|
|
429
|
+
const query = queryBuilder.insertSeedData(tableName, row);
|
|
430
|
+
const idempotentQuery = query + ' ON CONFLICT DO NOTHING';
|
|
431
|
+
this.context._execute(idempotentQuery);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Bulk seed data insertion (more efficient for multiple rows)
|
|
438
|
+
* @param {string} tableName - Name of the table
|
|
439
|
+
* @param {Array} rows - Array of data objects
|
|
440
|
+
*/
|
|
441
|
+
bulkSeed(tableName, rows){
|
|
442
|
+
if(!tableName || !rows || rows.length === 0){ return; }
|
|
443
|
+
|
|
444
|
+
if(this.context.isSQLite){
|
|
445
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
446
|
+
var queryBuilder = new sqliteQuery();
|
|
447
|
+
const query = queryBuilder.bulkInsertSeedData(tableName, rows);
|
|
448
|
+
if(query){
|
|
449
|
+
const idempotentQuery = query.replace(/^INSERT INTO/, 'INSERT OR IGNORE INTO');
|
|
450
|
+
this.context._execute(idempotentQuery);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if(this.context.isMySQL){
|
|
455
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
456
|
+
var queryBuilder = new sqlquery();
|
|
457
|
+
const query = queryBuilder.bulkInsertSeedData(tableName, rows);
|
|
458
|
+
if(query){
|
|
459
|
+
const idempotentQuery = query.replace(/^INSERT INTO/, 'INSERT IGNORE INTO');
|
|
460
|
+
this.context._execute(idempotentQuery);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if(this.context.isPostgres){
|
|
465
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
466
|
+
var queryBuilder = new postgresQuery();
|
|
467
|
+
const query = queryBuilder.bulkInsertSeedData(tableName, rows);
|
|
468
|
+
if(query){
|
|
469
|
+
const idempotentQuery = query + ' ON CONFLICT DO NOTHING';
|
|
470
|
+
this.context._execute(idempotentQuery);
|
|
367
471
|
}
|
|
368
|
-
this.context._execute(sql);
|
|
369
472
|
}
|
|
370
473
|
}
|
|
371
474
|
|
|
@@ -61,28 +61,28 @@ class queryMethods{
|
|
|
61
61
|
|
|
62
62
|
______orderByCount(query, ...args){
|
|
63
63
|
var str = query.toString();
|
|
64
|
-
str = this.
|
|
64
|
+
str = this.__validateAndCollectParameters(str, args, 'orderByCount');
|
|
65
65
|
this.__queryObject.orderByCount(str, this.__entity.__name);
|
|
66
66
|
return this;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
______orderByCountDescending(query, ...args){
|
|
70
70
|
var str = query.toString();
|
|
71
|
-
str = this.
|
|
71
|
+
str = this.__validateAndCollectParameters(str, args, 'orderByCountDescending');
|
|
72
72
|
this.__queryObject.orderByCountDesc(str, this.__entity.__name);
|
|
73
73
|
return this;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
orderBy(query, ...args){
|
|
77
77
|
var str = query.toString();
|
|
78
|
-
str = this.
|
|
78
|
+
str = this.__validateAndCollectParameters(str, args, 'orderBy');
|
|
79
79
|
this.__queryObject.orderBy(str, this.__entity.__name);
|
|
80
80
|
return this;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
orderByDescending(query, ...args){
|
|
84
84
|
var str = query.toString();
|
|
85
|
-
str = this.
|
|
85
|
+
str = this.__validateAndCollectParameters(str, args, 'orderByDescending');
|
|
86
86
|
this.__queryObject.orderByDesc(str, this.__entity.__name);
|
|
87
87
|
return this;
|
|
88
88
|
}
|
|
@@ -95,14 +95,22 @@ class queryMethods{
|
|
|
95
95
|
/* WHERE and AND work together its a way to add to the WHERE CLAUSE DYNAMICALLY */
|
|
96
96
|
and(query, ...args){
|
|
97
97
|
var str = query.toString();
|
|
98
|
-
|
|
98
|
+
// Transform .includes() syntax to .any() syntax
|
|
99
|
+
var transformResult = this.__transformIncludes(str, args);
|
|
100
|
+
str = transformResult.query;
|
|
101
|
+
args = transformResult.args;
|
|
102
|
+
str = this.__validateAndCollectParameters(str, args, 'and');
|
|
99
103
|
this.__queryObject.and(str, this.__entity.__name);
|
|
100
104
|
return this;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
where(query, ...args){
|
|
104
108
|
var str = query.toString();
|
|
105
|
-
|
|
109
|
+
// Transform .includes() syntax to .any() syntax
|
|
110
|
+
var transformResult = this.__transformIncludes(str, args);
|
|
111
|
+
str = transformResult.query;
|
|
112
|
+
args = transformResult.args;
|
|
113
|
+
str = this.__validateAndCollectParameters(str, args, 'where');
|
|
106
114
|
this.__queryObject.where(str, this.__entity.__name);
|
|
107
115
|
return this;
|
|
108
116
|
}
|
|
@@ -111,7 +119,7 @@ class queryMethods{
|
|
|
111
119
|
//Eagerly loading
|
|
112
120
|
include(query, ...args){
|
|
113
121
|
var str = query.toString();
|
|
114
|
-
str = this.
|
|
122
|
+
str = this.__validateAndCollectParameters(str, args, 'include');
|
|
115
123
|
this.__queryObject.include(str, this.__entity.__name);
|
|
116
124
|
return this;
|
|
117
125
|
}
|
|
@@ -119,7 +127,7 @@ class queryMethods{
|
|
|
119
127
|
// only takes a array of selected items
|
|
120
128
|
select(query, ...args){
|
|
121
129
|
var str = query.toString();
|
|
122
|
-
str = this.
|
|
130
|
+
str = this.__validateAndCollectParameters(str, args, 'select');
|
|
123
131
|
this.__queryObject.select(str, this.__entity.__name);
|
|
124
132
|
return this;
|
|
125
133
|
}
|
|
@@ -141,7 +149,7 @@ class queryMethods{
|
|
|
141
149
|
count(query, ...args){
|
|
142
150
|
if(query){
|
|
143
151
|
var str = query.toString();
|
|
144
|
-
str = this.
|
|
152
|
+
str = this.__validateAndCollectParameters(str, args, 'count');
|
|
145
153
|
this.__queryObject.count(str, this.__entity.__name);
|
|
146
154
|
}
|
|
147
155
|
|
|
@@ -152,7 +160,7 @@ class queryMethods{
|
|
|
152
160
|
this.__reset();
|
|
153
161
|
return val;
|
|
154
162
|
}
|
|
155
|
-
|
|
163
|
+
|
|
156
164
|
if(this.__context.isMySQL){
|
|
157
165
|
// trying to match string select and relace with select Count(*);
|
|
158
166
|
var entityValue = this.__context._SQLEngine.getCount(this.__queryObject, this.__entity, this.__context);
|
|
@@ -162,15 +170,58 @@ class queryMethods{
|
|
|
162
170
|
}
|
|
163
171
|
}
|
|
164
172
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Transform .includes() syntax to .any() syntax
|
|
175
|
+
* Converts: $$.includes(entity.field) => entity.field.any($$)
|
|
176
|
+
* This allows natural JavaScript array syntax while using existing .any() infrastructure
|
|
177
|
+
*/
|
|
178
|
+
__transformIncludes(str, args){
|
|
179
|
+
// Pattern: $$.includes(entity.field) or $$.includes(entity.field.nested)
|
|
180
|
+
const includesPattern = /\$\$\.includes\s*\(\s*([\w\d$_]+)\.([.\w\d_]+)\s*\)/g;
|
|
181
|
+
|
|
182
|
+
// Use replace with a function - when using a function, return value is used literally
|
|
183
|
+
const transformedStr = str.replace(includesPattern, (match, entity, field) => {
|
|
184
|
+
// Transform to .any() syntax: entity.field.any($$)
|
|
185
|
+
return entity + '.' + field + '.any($$)';
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return { query: transformedStr, args: args };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
__validateAndCollectParameters(str, args, methodName){
|
|
192
|
+
// Count placeholders - support both $$ (standard) and $ (backwards compatibility)
|
|
193
|
+
// Match $$ first to avoid double-counting, then match remaining single $
|
|
194
|
+
let placeholderCount = 0;
|
|
195
|
+
let tempStr = str;
|
|
196
|
+
|
|
197
|
+
// Count $$ placeholders first
|
|
198
|
+
const doubleDollarMatches = tempStr.match(/\$\$/g);
|
|
199
|
+
if(doubleDollarMatches){
|
|
200
|
+
placeholderCount += doubleDollarMatches.length;
|
|
201
|
+
// Remove $$ from string to avoid double-counting
|
|
202
|
+
tempStr = tempStr.replace(/\$\$/g, '');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Count remaining single $ placeholders
|
|
206
|
+
// Exclude $N (postgres placeholders like $1, $2) and $$ (already counted)
|
|
207
|
+
const singleDollarMatches = tempStr.match(/\$(?!\d)/g);
|
|
208
|
+
if(singleDollarMatches){
|
|
209
|
+
placeholderCount += singleDollarMatches.length;
|
|
210
|
+
}
|
|
211
|
+
|
|
168
212
|
const providedCount = args ? args.length : 0;
|
|
169
213
|
if(placeholderCount !== providedCount){
|
|
170
|
-
const msg = `Query argument error in ${methodName}: expected ${placeholderCount} value(s) for
|
|
214
|
+
const msg = `Query argument error in ${methodName}: expected ${placeholderCount} value(s) for parameter placeholders, but received ${providedCount}. Use $$ or $ for parameters.`;
|
|
171
215
|
console.error(msg);
|
|
172
216
|
throw new Error(msg);
|
|
173
217
|
}
|
|
218
|
+
|
|
219
|
+
// Get database type from context
|
|
220
|
+
const dbType = this.__context.isSQLite ? 'sqlite' :
|
|
221
|
+
this.__context.isMySQL ? 'mysql' :
|
|
222
|
+
this.__context.isPostgres ? 'postgres' : 'sqlite';
|
|
223
|
+
|
|
224
|
+
// Replace $$ with ? placeholders and collect parameter values
|
|
174
225
|
if(args){
|
|
175
226
|
for(let argument in args){
|
|
176
227
|
var item = args[argument];
|
|
@@ -179,12 +230,90 @@ class queryMethods{
|
|
|
179
230
|
console.error(msg);
|
|
180
231
|
throw new Error(msg);
|
|
181
232
|
}
|
|
182
|
-
|
|
233
|
+
|
|
234
|
+
// Check if this is an array (for IN clauses / .includes() / .any())
|
|
235
|
+
let itemArray = null;
|
|
236
|
+
if(Array.isArray(item)){
|
|
237
|
+
itemArray = item;
|
|
238
|
+
}
|
|
239
|
+
// Also handle comma-separated strings for .any() method
|
|
240
|
+
else if(typeof item === 'string' && item.includes(',')){
|
|
241
|
+
// Split comma-separated string into array
|
|
242
|
+
itemArray = item.split(',').map(v => v.trim());
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if(itemArray){
|
|
246
|
+
// Validate each array element
|
|
247
|
+
try {
|
|
248
|
+
for(const val of itemArray){
|
|
249
|
+
this.__queryObject.parameters.validateValue(val);
|
|
250
|
+
}
|
|
251
|
+
} catch(err) {
|
|
252
|
+
const msg = `Query argument error in ${methodName}: ${err.message}`;
|
|
253
|
+
console.error(msg);
|
|
254
|
+
throw new Error(msg);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add array parameters and get comma-separated placeholders
|
|
258
|
+
const placeholders = this.__queryObject.parameters.addParams(itemArray, dbType);
|
|
259
|
+
// Replace $$ first (preferred), then $ (backwards compatibility)
|
|
260
|
+
if(str.includes('$$')){
|
|
261
|
+
str = str.replace("$$", placeholders);
|
|
262
|
+
} else {
|
|
263
|
+
// Replace single $ but not $N (postgres placeholders)
|
|
264
|
+
str = str.replace(/\$(?!\d)/, placeholders);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else{
|
|
268
|
+
// Single value - existing logic
|
|
269
|
+
// Validate parameter value is safe
|
|
270
|
+
try {
|
|
271
|
+
this.__queryObject.parameters.validateValue(item);
|
|
272
|
+
} catch(err) {
|
|
273
|
+
const msg = `Query argument error in ${methodName}: ${err.message}`;
|
|
274
|
+
console.error(msg);
|
|
275
|
+
throw new Error(msg);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Add parameter and replace placeholder
|
|
279
|
+
const placeholder = this.__queryObject.parameters.addParam(item, dbType);
|
|
280
|
+
// Replace $$ first (preferred), then $ (backwards compatibility)
|
|
281
|
+
if(str.includes('$$')){
|
|
282
|
+
str = str.replace("$$", placeholder);
|
|
283
|
+
} else {
|
|
284
|
+
// Replace single $ but not $N (postgres placeholders)
|
|
285
|
+
str = str.replace(/\$(?!\d)/, placeholder);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
183
288
|
}
|
|
184
289
|
}
|
|
185
290
|
return str;
|
|
186
291
|
}
|
|
187
292
|
|
|
293
|
+
// Convenience method: Find record by primary key ID
|
|
294
|
+
findById(id){
|
|
295
|
+
// Find the primary key field in the entity
|
|
296
|
+
let primaryKeyField = null;
|
|
297
|
+
for (const fieldName in this.__entity) {
|
|
298
|
+
const field = this.__entity[fieldName];
|
|
299
|
+
if (field && field.primary === true) {
|
|
300
|
+
primaryKeyField = fieldName;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!primaryKeyField) {
|
|
306
|
+
throw new Error(`findById error: No primary key defined on entity '${this.__entity.__name}'`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Build where clause: entity.primaryKey == id
|
|
310
|
+
const entityParam = 'r'; // Standard parameter name
|
|
311
|
+
const whereClause = `${entityParam} => ${entityParam}.${primaryKeyField} == $$`;
|
|
312
|
+
|
|
313
|
+
// Chain where() and single()
|
|
314
|
+
return this.where(whereClause, id).single();
|
|
315
|
+
}
|
|
316
|
+
|
|
188
317
|
single(){
|
|
189
318
|
// If no clauses were used before single(), seed defaults so SQL is valid
|
|
190
319
|
if(this.__queryObject.script.entityMap.length === 0){
|
|
@@ -237,6 +366,55 @@ class queryMethods{
|
|
|
237
366
|
|
|
238
367
|
// ------------------------------- FUNCTIONS THAT UPDATE SQL START FROM HERE -----------------------------------------------------
|
|
239
368
|
// ---------------------------------------------------------------------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
// Creates a new empty entity instance ready for insertion
|
|
371
|
+
// Returns an object with property setters that track changes
|
|
372
|
+
new(){
|
|
373
|
+
var newEntity = {
|
|
374
|
+
__ID : Math.floor((Math.random() * 100000) + 1),
|
|
375
|
+
__dirtyFields : [],
|
|
376
|
+
__state : "insert",
|
|
377
|
+
__entity : this.__entity,
|
|
378
|
+
__context : this.__context,
|
|
379
|
+
__name : this.__entity.__name,
|
|
380
|
+
__proto__ : {}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// Set up property setters for all entity fields
|
|
384
|
+
var $that = this;
|
|
385
|
+
for (var fieldName in this.__entity) {
|
|
386
|
+
if(!fieldName.startsWith("__")){
|
|
387
|
+
var field = this.__entity[fieldName];
|
|
388
|
+
// Skip navigational properties (relationships)
|
|
389
|
+
if(!field.isNavigational && field.type !== "hasMany" && field.type !== "hasOne" && field.type !== "hasManyThrough"){
|
|
390
|
+
(function(fname, fieldDef){
|
|
391
|
+
Object.defineProperty(newEntity, fname, {
|
|
392
|
+
enumerable: true,
|
|
393
|
+
configurable: true,
|
|
394
|
+
set: function(value) {
|
|
395
|
+
this.__proto__["_" + fname] = value;
|
|
396
|
+
if(!this.__dirtyFields.includes(fname)){
|
|
397
|
+
this.__dirtyFields.push(fname);
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
get: function(){
|
|
401
|
+
// Apply get function if defined
|
|
402
|
+
if(fieldDef && typeof fieldDef.get === "function"){
|
|
403
|
+
return fieldDef.get(this.__proto__["_" + fname]);
|
|
404
|
+
}
|
|
405
|
+
return this.__proto__["_" + fname];
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
})(fieldName, field);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Track the entity
|
|
414
|
+
this.__context.__track(newEntity);
|
|
415
|
+
return newEntity;
|
|
416
|
+
}
|
|
417
|
+
|
|
240
418
|
add(entityValue){
|
|
241
419
|
entityValue.__state = "insert";
|
|
242
420
|
entityValue.__entity = this.__entity;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// QueryParameters - Manages parameterized query values
|
|
2
|
+
// Version 1.0.0
|
|
3
|
+
// Provides SQL injection protection through proper parameterization
|
|
4
|
+
|
|
5
|
+
class QueryParameters {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.params = [];
|
|
8
|
+
this.paramIndex = 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Add a parameter value and return the placeholder
|
|
13
|
+
* @param {*} value - The value to add
|
|
14
|
+
* @param {string} dbType - 'sqlite', 'mysql', or 'postgres'
|
|
15
|
+
* @returns {string} - The placeholder (? for MySQL/SQLite, $1 for Postgres)
|
|
16
|
+
*/
|
|
17
|
+
addParam(value, dbType = 'sqlite') {
|
|
18
|
+
// Validate value is not undefined
|
|
19
|
+
if (typeof value === 'undefined') {
|
|
20
|
+
throw new Error(`Parameter value cannot be undefined at index ${this.paramIndex}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Add to parameters array
|
|
24
|
+
this.params.push(value);
|
|
25
|
+
this.paramIndex++;
|
|
26
|
+
|
|
27
|
+
// Return appropriate placeholder based on database type
|
|
28
|
+
switch(dbType.toLowerCase()) {
|
|
29
|
+
case 'postgres':
|
|
30
|
+
return `$${this.paramIndex}`; // $1, $2, $3...
|
|
31
|
+
case 'mysql':
|
|
32
|
+
case 'sqlite':
|
|
33
|
+
default:
|
|
34
|
+
return '?'; // ? for MySQL and SQLite
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add multiple parameters (for IN clauses)
|
|
40
|
+
* @param {Array} values - Array of values
|
|
41
|
+
* @param {string} dbType - Database type
|
|
42
|
+
* @returns {string} - Comma-separated placeholders like "?, ?, ?"
|
|
43
|
+
*/
|
|
44
|
+
addParams(values, dbType = 'sqlite') {
|
|
45
|
+
if (!Array.isArray(values)) {
|
|
46
|
+
throw new Error('addParams expects an array');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (values.length === 0) {
|
|
50
|
+
throw new Error('Cannot create IN clause with empty array');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const placeholders = [];
|
|
54
|
+
for (const value of values) {
|
|
55
|
+
placeholders.push(this.addParam(value, dbType));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return placeholders.join(', ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get all collected parameters
|
|
63
|
+
* @returns {Array} - Array of parameter values
|
|
64
|
+
*/
|
|
65
|
+
getParams() {
|
|
66
|
+
return this.params;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get parameter count
|
|
71
|
+
* @returns {number} - Number of parameters
|
|
72
|
+
*/
|
|
73
|
+
count() {
|
|
74
|
+
return this.params.length;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reset parameters (called after query execution)
|
|
79
|
+
*/
|
|
80
|
+
reset() {
|
|
81
|
+
this.params = [];
|
|
82
|
+
this.paramIndex = 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Merge parameters from another QueryParameters instance
|
|
87
|
+
* Used when combining multiple query clauses
|
|
88
|
+
* @param {QueryParameters} other - Another QueryParameters instance
|
|
89
|
+
*/
|
|
90
|
+
merge(other) {
|
|
91
|
+
if (other && other.params && Array.isArray(other.params)) {
|
|
92
|
+
this.params.push(...other.params);
|
|
93
|
+
this.paramIndex += other.params.length;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Validate a value is safe for parameterization
|
|
99
|
+
* Rejects functions, symbols, and other unsafe types
|
|
100
|
+
* @param {*} value - Value to validate
|
|
101
|
+
* @returns {boolean} - True if valid
|
|
102
|
+
* @throws {Error} - If value is invalid
|
|
103
|
+
*/
|
|
104
|
+
validateValue(value) {
|
|
105
|
+
const type = typeof value;
|
|
106
|
+
|
|
107
|
+
// Allow null (SQL NULL)
|
|
108
|
+
if (value === null) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Allow primitives
|
|
113
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Allow Date objects
|
|
118
|
+
if (value instanceof Date) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Allow Buffer (for binary data)
|
|
123
|
+
if (Buffer.isBuffer(value)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Reject everything else (functions, objects, symbols, etc.)
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Invalid parameter type: ${type}. ` +
|
|
130
|
+
`Only primitives, null, Date, and Buffer are allowed. ` +
|
|
131
|
+
`Received: ${JSON.stringify(value)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = QueryParameters;
|