masterrecord 0.1.4 → 0.2.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/SQLLiteEngine.js CHANGED
@@ -1,4 +1,4 @@
1
- // Version 0.0.13
1
+ // Version 0.0.23
2
2
  var tools = require('masterrecord/Tools');
3
3
 
4
4
  class SQLLiteEngine {
@@ -55,6 +55,23 @@ class SQLLiteEngine {
55
55
  }
56
56
  }
57
57
 
58
+ // Introspection helpers
59
+ tableExists(tableName){
60
+ try{
61
+ const sql = `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`;
62
+ const row = this.db.prepare(sql).get();
63
+ return !!row;
64
+ }catch(_){ return false; }
65
+ }
66
+
67
+ getTableInfo(tableName){
68
+ try{
69
+ const sql = `PRAGMA table_info(${tableName})`;
70
+ const rows = this.db.prepare(sql).all();
71
+ return rows || [];
72
+ }catch(_){ return []; }
73
+ }
74
+
58
75
  getCount(queryObject, entity, context){
59
76
  var query = queryObject.script;
60
77
  var queryString = {};
@@ -256,54 +273,74 @@ class SQLLiteEngine {
256
273
  buildWhere(query, mainQuery){
257
274
  var whereEntity = query.where;
258
275
 
259
- var strQuery = "";
260
276
  var $that = this;
261
- if(whereEntity){
262
- var entity = this.getEntity(query.parentName, query.entityMap);
277
+ if(!whereEntity){
278
+ return "";
279
+ }
263
280
 
264
- var item = whereEntity[query.parentName].query;
265
- for (let exp in item.expressions) {
266
- var field = tools.capitalizeFirstLetter(item.expressions[exp].field);
267
- if(mainQuery[field]){
268
- if(mainQuery[field].isNavigational){
269
- entity = $that.getEntity(field, query.entityMap);
281
+ var entityAlias = this.getEntity(query.parentName, query.entityMap);
282
+ var item = whereEntity[query.parentName].query;
283
+ var exprs = item.expressions || [];
284
+
285
+ function exprToSql(expr){
286
+ var field = expr.field.toLowerCase();
287
+ var ent = entityAlias;
288
+ if(mainQuery[field]){
289
+ if(mainQuery[field].isNavigational){
290
+ ent = $that.getEntity(field, query.entityMap);
291
+ // field alias fallback kept as original logic; if item.fields exists, use second
292
+ if(item.fields && item.fields[1]){
270
293
  field = item.fields[1];
271
294
  }
272
295
  }
273
- if(item.expressions[exp].arg === "null"){
274
- if(item.expressions[exp].func === "="){
275
- item.expressions[exp].func = "is"
276
- }
277
- if(item.expressions[exp].func === "!="){
278
- item.expressions[exp].func = "is not"
279
- }
296
+ }
297
+ let func = expr.func;
298
+ let arg = expr.arg;
299
+ if((!func && typeof arg === 'undefined')){
300
+ return null;
301
+ }
302
+ // Removed fallback that coerced 'exists' with an argument to '='
303
+ // Bare field or !field: interpret as IS [NOT] NULL for SQLite
304
+ if(func === 'exists' && typeof arg === 'undefined'){
305
+ const isNull = expr.negate === true; // '!field' -> IS NULL
306
+ return `${ent}.${field} is ${isNull ? '' : 'not '}null`;
307
+ }
308
+ if(arg === "null"){
309
+ if(func === "=") func = "is";
310
+ if(func === "!=") func = "is not";
311
+ return `${ent}.${field} ${func} ${arg}`;
312
+ }
313
+ if(func === "IN"){
314
+ return `${ent}.${field} ${func} ${arg}`;
315
+ }
316
+ return `${ent}.${field} ${func} '${arg}'`;
317
+ }
318
+
319
+ const pieces = [];
320
+ for(let i = 0; i < exprs.length; i++){
321
+ const e = exprs[i];
322
+ if(e.group){
323
+ const gid = e.group;
324
+ const orParts = [];
325
+ while(i < exprs.length && exprs[i].group === gid){
326
+ const sql = exprToSql(exprs[i]);
327
+ if(sql){ orParts.push(sql); }
328
+ i++;
280
329
  }
281
- if(strQuery === ""){
282
- if(item.expressions[exp].arg === "null"){
283
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
284
- }else{
285
- if(item.expressions[exp].func === "IN"){
286
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
287
- }
288
- else{
289
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
290
- }
291
- }
292
- }
293
- else{
294
- if(item.expressions[exp].arg === "null"){
295
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} ${item.expressions[exp].arg}`;
296
- }else{
297
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
298
- }
299
-
330
+ i--; // step back one since for-loop will increment
331
+ if(orParts.length > 0){
332
+ pieces.push(`(${orParts.join(" or ")})`);
300
333
  }
334
+ }else{
335
+ const sql = exprToSql(e);
336
+ if(sql){ pieces.push(sql); }
301
337
  }
338
+ }
302
339
 
303
-
304
-
340
+ if(pieces.length === 0){
341
+ return "";
305
342
  }
306
- return strQuery;
343
+ return `WHERE ${pieces.join(" and ")}`;
307
344
  }
308
345
 
309
346
  buildInclude( query, entity, context){
@@ -464,6 +501,14 @@ class SQLLiteEngine {
464
501
  }
465
502
  }
466
503
  }
504
+ // Ensure primary key is always included in SELECT list
505
+ try{
506
+ const pk = this.getPrimarykey(entity);
507
+ if(pk){
508
+ const hasPk = entitiesList.indexOf(pk) !== -1 || entitiesList.indexOf(`[${pk}]`) !== -1 || entitiesList.indexOf(`'${pk}'`) !== -1;
509
+ if(!hasPk){ entitiesList.unshift(pk); }
510
+ }
511
+ }catch(_){ /* ignore */ }
467
512
  return entitiesList
468
513
  }
469
514
  chechUnsupportedWords(word){
@@ -495,15 +540,47 @@ class SQLLiteEngine {
495
540
 
496
541
  for (var column in dirtyFields) {
497
542
 
543
+ // Validate non-nullable constraints on updates
544
+ var fieldName = dirtyFields[column];
545
+ var entityDef = model.__entity[fieldName];
546
+ if(entityDef && entityDef.nullable === false && entityDef.primary !== true){
547
+ // Determine the value that will actually be persisted for this field
548
+ var persistedValue;
549
+ switch(entityDef.type){
550
+ case "integer":
551
+ persistedValue = model["_" + fieldName];
552
+ break;
553
+ case "belongsTo":
554
+ persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName];
555
+ break;
556
+ default:
557
+ persistedValue = model[fieldName];
558
+ }
559
+ var isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
560
+ if(persistedValue === undefined || persistedValue === null || isEmptyString){
561
+ throw `Entity ${model.__entity.__name} column ${fieldName} is a required Field`;
562
+ }
563
+ }
564
+
565
+ var type = model.__entity[dirtyFields[column]].type;
566
+
567
+ if(model.__entity[dirtyFields[column]].relationshipType === "belongsTo"){
568
+ type = "belongsTo";
569
+ }
498
570
  // TODO Boolean value is a string with a letter
499
- switch(model.__entity[dirtyFields[column]].type){
571
+ switch(type){
572
+ case "belongsTo" :
573
+ const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
574
+ argument = `${foreignKey} = ${model[dirtyFields[column]]},`;
575
+ break;
500
576
  case "integer" :
501
- //model.__entity[dirtyFields[column]].skipGetFunction = true;
502
- argument = argument === null ? `[${dirtyFields[column]}] = ${model[dirtyFields[column]]},` : `${argument} [${dirtyFields[column]}] = ${model[dirtyFields[column]]},`;
577
+ //model.__entity[dirtyFields[column]].skipGetFunction = true;
578
+ var columneValue = model[`_${dirtyFields[column]}`];
579
+ argument = argument === null ? `[${dirtyFields[column]}] = ${model[dirtyFields[column]]},` : `${argument} [${dirtyFields[column]}] = ${columneValue},`;
503
580
  //model.__entity[dirtyFields[column]].skipGetFunction = false;
504
581
  break;
505
582
  case "string" :
506
- argument = argument === null ? `[${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]])}',` : `${argument} [${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]])}',`;
583
+ argument = argument === null ? `[${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]], { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',` : `${argument} [${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]], { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',`;
507
584
  break;
508
585
  case "boolean" :
509
586
  var bool = "";
@@ -513,10 +590,10 @@ class SQLLiteEngine {
513
590
  else{
514
591
  bool = model[dirtyFields[column]];
515
592
  }
516
- argument = argument === null ? `[${dirtyFields[column]}] = '${bool}',` : `${argument} [${dirtyFields[column]}] = ${bool},`;
593
+ argument = argument === null ? `[${dirtyFields[column]}] = '${bool}',` : `${argument} [${dirtyFields[column]}] = '${bool}',`;
517
594
  break;
518
595
  case "time" :
519
- argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = ${model[dirtyFields[column]]},`;
596
+ argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = '${model[dirtyFields[column]]}',`;
520
597
  break;
521
598
  case "belongsTo" :
522
599
  var fore = `_${dirtyFields[column]}`;
@@ -529,7 +606,14 @@ class SQLLiteEngine {
529
606
  argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = '${model[dirtyFields[column]]}',`;
530
607
  }
531
608
  }
532
- return argument.replace(/,\s*$/, "");
609
+
610
+ if(argument){
611
+ return argument.replace(/,\s*$/, "");
612
+ }
613
+ else{
614
+ return -1;
615
+ }
616
+
533
617
  }
534
618
 
535
619
 
@@ -554,20 +638,23 @@ class SQLLiteEngine {
554
638
  // check if get function is avaliable if so use that
555
639
  fieldColumn = fields[column];
556
640
 
557
- if((fieldColumn !== undefined && fieldColumn !== null && fieldColumn !== "" ) && typeof(fieldColumn) !== "object"){
641
+ if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
558
642
  switch(modelEntity[column].type){
559
- case "belongsTo" :
560
- column = modelEntity[column].foreignKey === undefined ? column : modelEntity[column].foreignKey;
561
- break;
562
643
  case "string" :
563
- fieldColumn = `'${$that._santizeSingleQuotes(fields[column])}'`;
644
+ fieldColumn = `'${$that._santizeSingleQuotes(fields[column], { entityName: modelEntity.__name, fieldName: column })}'`;
564
645
  break;
565
646
  case "time" :
566
647
  fieldColumn = fields[column];
567
648
  break;
568
649
  }
569
650
 
570
- columns = columns === null ? `'${column}',` : `${columns} '${column}',`;
651
+ var relationship = modelEntity[column].relationshipType
652
+ if(relationship === "belongsTo"){
653
+ column = modelEntity[column].foreignKey
654
+ }
655
+
656
+ // Use bracket-quoted identifiers for SQLite column names
657
+ columns = columns === null ? `[${column}],` : `${columns} [${column}],`;
571
658
  values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
572
659
 
573
660
  }
@@ -579,7 +666,8 @@ class SQLLiteEngine {
579
666
  var primaryKey = tools.getPrimaryKeyObject(fieldObject.__entity);
580
667
  fieldColumn = fieldObject[primaryKey];
581
668
  column = modelEntity[column].foreignKey;
582
- columns = columns === null ? `'${column}',` : `${columns} '${column}',`;
669
+ // Use bracket-quoted identifiers for SQLite column names
670
+ columns = columns === null ? `[${column}],` : `${columns} [${column}],`;
583
671
  values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
584
672
  }else{
585
673
  console.log("Cannot find belings to relationship")
@@ -595,15 +683,24 @@ class SQLLiteEngine {
595
683
 
596
684
  }
597
685
 
598
- // will add double single quotes to allow sting to be saved.
599
- _santizeSingleQuotes(string){
600
- if (typeof string === 'string' || string instanceof String){
601
- return string.replace(/'/g, "''");
686
+ // will add double single quotes to allow string to be saved.
687
+ _santizeSingleQuotes(value, context){
688
+ if (typeof value === 'string' || value instanceof String){
689
+ return value.replace(/'/g, "''");
690
+ }
691
+ else{
692
+ var details = context || {};
693
+ var entityName = details.entityName || 'UnknownEntity';
694
+ var fieldName = details.fieldName || 'UnknownField';
695
+ var valueType = (value === null) ? 'null' : (value === undefined ? 'undefined' : typeof value);
696
+ var preview;
697
+ try{ preview = (value === null || value === undefined) ? String(value) : JSON.stringify(value); }
698
+ catch(_){ preview = '[unserializable]'; }
699
+ if(preview && preview.length > 120){ preview = preview.substring(0, 120) + '…'; }
700
+ var message = `Field is not a string: entity=${entityName}, field=${fieldName}, type=${valueType}, value=${preview}`;
701
+ console.error(message);
702
+ throw new Error(message);
602
703
  }
603
- else{
604
- console.log("warning - Field being passed is not a string");
605
- throw "warning - Field being passed is not a string";
606
- }
607
704
  }
608
705
 
609
706
  // converts any object into SQL parameter select string
package/Tools.js CHANGED
@@ -1,6 +1,36 @@
1
- // Version 0.0.3
1
+ // Version 0.0.5
2
2
  class Tools{
3
3
 
4
+ static checkIfArrayLike(obj) {
5
+ if (Array.isArray(obj)) {
6
+ return true;
7
+ }
8
+
9
+ if (
10
+ obj &&
11
+ typeof obj === 'object' &&
12
+ Object.keys(obj).some(k => !isNaN(k)) &&
13
+ '0' in obj
14
+ ) {
15
+ return true;
16
+ }
17
+
18
+ return -1;
19
+ }
20
+
21
+ static returnEntityList(list, entityList ){
22
+ var newList = [];
23
+ for(var max = 0; max < list.length; max++ ){
24
+ var ent = entityList[list[max]];
25
+ if(ent){
26
+ if(ent.relationshipType === "hasMany" || ent.relationshipType === "hasOne"){
27
+ newList.push(ent.name);
28
+ }
29
+ }
30
+ }
31
+ return newList;
32
+ }
33
+
4
34
  static findEntity(name, entityList){
5
35
  return entityList[name];
6
36
  }
@@ -15,25 +45,6 @@ class Tools{
15
45
  return stringArray.join(type);
16
46
  }
17
47
 
18
- static removePrimarykeyandVirtual(currentModel, modelEntity){
19
- var newCurrentModel = Object.create(currentModel);
20
-
21
- for(var entity in modelEntity) {
22
- var currentEntity = modelEntity[entity];
23
- if (modelEntity.hasOwnProperty(entity)) {
24
- if(currentEntity.primary === true){
25
- delete newCurrentModel[`_${entity}`];
26
- }
27
- }
28
- if(currentEntity.virtual === true){
29
- // skip it from the insert
30
- delete newCurrentModel[`_${entity}`];
31
- }
32
-
33
- }
34
- return newCurrentModel;
35
- }
36
-
37
48
  static getPrimaryKeyObject(model){
38
49
  for (var key in model) {
39
50
  if (model.hasOwnProperty(key)) {
@@ -73,20 +84,54 @@ class Tools{
73
84
  }
74
85
 
75
86
  static clearAllProto(proto){
87
+
88
+ var newproto = {}
76
89
  if(proto.__proto__ ){
77
- proto.__proto__ = null;
78
- for (var key in proto) {
79
- if(!key.startsWith("_")){
80
- var typeObj = typeof(proto[key]);
81
- if(typeObj === "object"){
82
- this.clearAllProto(proto[key]);
83
- }
84
- }else{
85
- throw "Cannot add relationship entity model only basic models"
90
+ // Include non-enumerable own properties so we don't lose values defined via getters
91
+ const keys = Object.getOwnPropertyNames(proto);
92
+ for (const key of keys) {
93
+ if(!key.startsWith("_") && !key.startsWith("__")){
94
+ try{
95
+ const value = proto[key];
96
+ if(typeof value === "object" && value !== null){
97
+ // Recursively clone nested objects without altering the source
98
+ newproto[key] = this.clearAllProto(value);
99
+ } else {
100
+ newproto[key] = value;
101
+ }
102
+ }catch(_){ /* ignore getter errors */ }
86
103
  }
87
104
  }
88
105
  }
89
106
 
107
+ newproto["__name"] = proto["__name"];
108
+ newproto["__state"] = proto["__state"];
109
+ newproto["__entity"] = proto["__entity"];
110
+ newproto["__context"] = proto["__context"];
111
+ newproto["__dirtyFields"] = proto["__dirtyFields"];
112
+
113
+ newproto.__proto__ = null;
114
+ return newproto;
115
+
116
+ }
117
+
118
+ static removePrimarykeyandVirtual(currentModel, modelEntity){
119
+ var newCurrentModel = Object.create(currentModel);
120
+
121
+ for(var entity in modelEntity) {
122
+ var currentEntity = modelEntity[entity];
123
+ if (modelEntity.hasOwnProperty(entity)) {
124
+ if(currentEntity.primary === true){
125
+ delete newCurrentModel[`_${entity}`];
126
+ }
127
+ }
128
+ if(currentEntity.virtual === true){
129
+ // skip it from the insert
130
+ delete newCurrentModel[`_${entity}`];
131
+ }
132
+
133
+ }
134
+ return newCurrentModel;
90
135
  }
91
136
 
92
137
  static getEntity(name, modelEntity){