masterrecord 0.1.4 → 0.2.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.
- package/Entity/entityTrackerModel.js +7 -3
- package/MIGRATIONS.md +178 -0
- package/Migrations/cli.js +3 -3
- package/Migrations/migrationMySQLQuery.js +18 -8
- package/Migrations/migrationSQLiteQuery.js +30 -3
- package/Migrations/schema.js +201 -16
- package/QueryLanguage/queryMethods.js +45 -58
- package/QueryLanguage/queryScript.js +102 -35
- package/SQLLiteEngine.js +158 -61
- package/Tools.js +74 -29
- package/context.js +191 -60
- package/deleteManager.js +3 -3
- package/insertManager.js +128 -34
- package/mySQLEngine.js +159 -44
- package/mySQLSyncConnect.js +2 -1
- package/package.json +5 -5
- package/readme.md +3 -1
package/SQLLiteEngine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Version 0.0.
|
|
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
|
-
|
|
277
|
+
if(!whereEntity){
|
|
278
|
+
return "";
|
|
279
|
+
}
|
|
263
280
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
502
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
599
|
-
_santizeSingleQuotes(
|
|
600
|
-
if (typeof
|
|
601
|
-
return
|
|
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.
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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){
|