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.
Files changed (38) hide show
  1. package/.claude/settings.local.json +20 -1
  2. package/Entity/entityModel.js +6 -0
  3. package/Entity/entityTrackerModel.js +20 -3
  4. package/Entity/fieldTransformer.js +266 -0
  5. package/Migrations/migrationMySQLQuery.js +145 -1
  6. package/Migrations/migrationPostgresQuery.js +402 -0
  7. package/Migrations/migrationSQLiteQuery.js +145 -1
  8. package/Migrations/schema.js +131 -28
  9. package/QueryLanguage/queryMethods.js +193 -15
  10. package/QueryLanguage/queryParameters.js +136 -0
  11. package/QueryLanguage/queryScript.js +13 -4
  12. package/SQLLiteEngine.js +331 -20
  13. package/context.js +91 -14
  14. package/docs/INCLUDES_CLARIFICATION.md +202 -0
  15. package/docs/METHODS_REFERENCE.md +184 -0
  16. package/docs/MIGRATIONS_GUIDE.md +699 -0
  17. package/docs/POSTGRESQL_SETUP.md +415 -0
  18. package/examples/jsonArrayTransformer.js +215 -0
  19. package/mySQLEngine.js +273 -17
  20. package/package.json +3 -3
  21. package/postgresEngine.js +600 -483
  22. package/postgresSyncConnect.js +209 -0
  23. package/readme.md +1046 -416
  24. package/test/anyCommaStringTest.js +237 -0
  25. package/test/anyMethodTest.js +176 -0
  26. package/test/findByIdTest.js +227 -0
  27. package/test/includesFeatureTest.js +183 -0
  28. package/test/includesTransformTest.js +110 -0
  29. package/test/newMethodTest.js +330 -0
  30. package/test/newMethodUnitTest.js +320 -0
  31. package/test/parameterizedPlaceholderTest.js +159 -0
  32. package/test/postgresEngineTest.js +463 -0
  33. package/test/postgresIntegrationTest.js +381 -0
  34. package/test/securityTest.js +268 -0
  35. package/test/singleDollarPlaceholderTest.js +238 -0
  36. package/test/transformerTest.js +287 -0
  37. package/test/verifyFindById.js +169 -0
  38. package/test/verifyNewMethod.js +191 -0
@@ -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
- const check = admin.query(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`);
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
- // Create with sensible defaults
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
- for(const row of items){
344
- const cols = Object.keys(row);
345
- if(cols.length === 0){ continue; }
346
- const colList = cols.map(c => this.context.isSQLite ? `[${c}]` : `${c}`).join(", ");
347
- const vals = cols.map(k => {
348
- const v = row[k];
349
- if(v === null || v === undefined){ return 'NULL'; }
350
- if(typeof v === 'boolean'){
351
- return this.context.isSQLite ? (v ? 1 : 0) : (v ? 1 : 0);
352
- }
353
- if(typeof v === 'number'){
354
- return String(v);
355
- }
356
- const esc = String(v).replace(/'/g, "''");
357
- return `'${esc}'`;
358
- }).join(", ");
359
- // Idempotent seed: ignore duplicates on unique indexes
360
- let sql;
361
- if(this.context.isSQLite){
362
- sql = `INSERT OR IGNORE INTO ${tableName} (${colList}) VALUES (${vals})`;
363
- } else if(this.context.isMySQL){
364
- sql = `INSERT IGNORE INTO ${tableName} (${colList}) VALUES (${vals})`;
365
- } else {
366
- sql = `INSERT INTO ${tableName} (${colList}) VALUES (${vals})`;
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.__validateAndReplacePlaceholders(str, args, 'orderByCount');
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.__validateAndReplacePlaceholders(str, args, 'orderByCountDescending');
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.__validateAndReplacePlaceholders(str, args, 'orderBy');
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.__validateAndReplacePlaceholders(str, args, 'orderByDescending');
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
- str = this.__validateAndReplacePlaceholders(str, args, 'and');
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
- str = this.__validateAndReplacePlaceholders(str, args, 'where');
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.__validateAndReplacePlaceholders(str, args, 'include');
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.__validateAndReplacePlaceholders(str, args, 'select');
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.__validateAndReplacePlaceholders(str, args, 'count');
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
- __validateAndReplacePlaceholders(str, args, methodName){
166
- // Count placeholders
167
- const placeholderCount = (str.match(/\$\$/g) || []).length;
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 '$$', but received ${providedCount}.`;
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
- str = str.replace("$$", item);
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;