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/mySQLEngine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// version : 0.0.
|
|
1
|
+
// version : 0.0.9
|
|
2
|
+
|
|
2
3
|
var tools = require('masterrecord/Tools');
|
|
3
4
|
var util = require('util');
|
|
4
5
|
|
|
@@ -97,6 +98,25 @@ class SQLLiteEngine {
|
|
|
97
98
|
}
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Introspection helpers
|
|
102
|
+
tableExists(tableName){
|
|
103
|
+
try{
|
|
104
|
+
const sql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${tableName}'`;
|
|
105
|
+
this.db.connect(this.db);
|
|
106
|
+
const res = this.db.query(sql);
|
|
107
|
+
return Array.isArray(res) ? res.length > 0 : !!res?.length;
|
|
108
|
+
}catch(_){ return false; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
getTableInfo(tableName){
|
|
112
|
+
try{
|
|
113
|
+
const sql = `SELECT COLUMN_NAME as name, COLUMN_DEFAULT as dflt_value, IS_NULLABLE as is_nullable, DATA_TYPE as data_type FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${tableName}'`;
|
|
114
|
+
this.db.connect(this.db);
|
|
115
|
+
const res = this.db.query(sql);
|
|
116
|
+
return res || [];
|
|
117
|
+
}catch(_){ return []; }
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
|
|
101
121
|
buildQuery(query, entity, context){
|
|
102
122
|
|
|
@@ -122,32 +142,78 @@ class SQLLiteEngine {
|
|
|
122
142
|
}
|
|
123
143
|
|
|
124
144
|
buildWhere(query, mainQuery){
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
var whereEntity = query.where;
|
|
146
|
+
var $that = this;
|
|
147
|
+
if(!whereEntity){
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var entityAlias = this.getEntity(query.parentName, query.entityMap);
|
|
152
|
+
var item = whereEntity[query.parentName].query;
|
|
153
|
+
var exprs = item.expressions || [];
|
|
154
|
+
|
|
155
|
+
function exprToSql(expr){
|
|
156
|
+
var field = expr.field.toLowerCase();
|
|
157
|
+
var ent = entityAlias;
|
|
158
|
+
if(mainQuery[field]){
|
|
159
|
+
if(mainQuery[field].isNavigational){
|
|
160
|
+
ent = $that.getEntity(field, query.entityMap);
|
|
161
|
+
if(item.fields && item.fields[1]){
|
|
162
|
+
field = item.fields[1];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
let func = expr.func;
|
|
167
|
+
let arg = expr.arg;
|
|
168
|
+
if((!func && typeof arg === 'undefined')){
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
// Removed fallback that coerced 'exists' with an argument to '='
|
|
172
|
+
// Bare field or !field: interpret as IS [NOT] NULL
|
|
173
|
+
if(func === 'exists' && typeof arg === 'undefined'){
|
|
174
|
+
const isNull = expr.negate === true; // '!field' -> IS NULL
|
|
175
|
+
return `${ent}.${field} is ${isNull ? '' : 'not '}null`;
|
|
176
|
+
}
|
|
177
|
+
if(arg === "null"){
|
|
178
|
+
if(func === "=") func = "is";
|
|
179
|
+
if(func === "!=") func = "is not";
|
|
180
|
+
return `${ent}.${field} ${func} ${arg}`;
|
|
181
|
+
}
|
|
182
|
+
if(func === "IN"){
|
|
183
|
+
return `${ent}.${field} ${func} ${arg}`;
|
|
184
|
+
}
|
|
185
|
+
var safeArg = (typeof arg === 'string' || arg instanceof String)
|
|
186
|
+
? $that._santizeSingleQuotes(arg, { entityName: ent, fieldName: field })
|
|
187
|
+
: String(arg);
|
|
188
|
+
return `${ent}.${field} ${func} '${safeArg}'`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const pieces = [];
|
|
192
|
+
for(let i = 0; i < exprs.length; i++){
|
|
193
|
+
const e = exprs[i];
|
|
194
|
+
if(e.group){
|
|
195
|
+
const gid = e.group;
|
|
196
|
+
const orParts = [];
|
|
197
|
+
while(i < exprs.length && exprs[i].group === gid){
|
|
198
|
+
const sql = exprToSql(exprs[i]);
|
|
199
|
+
if(sql){ orParts.push(sql); }
|
|
200
|
+
i++;
|
|
201
|
+
}
|
|
202
|
+
i--; // compensate for loop increment
|
|
203
|
+
if(orParts.length > 0){
|
|
204
|
+
pieces.push(`(${orParts.join(" or ")})`);
|
|
205
|
+
}
|
|
206
|
+
}else{
|
|
207
|
+
const sql = exprToSql(e);
|
|
208
|
+
if(sql){ pieces.push(sql); }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if(pieces.length === 0){
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
return `WHERE ${pieces.join(" and ")}`;
|
|
216
|
+
}
|
|
151
217
|
|
|
152
218
|
buildInclude( query, entity, context){
|
|
153
219
|
var includeQuery = "";
|
|
@@ -307,6 +373,14 @@ class SQLLiteEngine {
|
|
|
307
373
|
}
|
|
308
374
|
}
|
|
309
375
|
}
|
|
376
|
+
// Ensure primary key is always included in SELECT list
|
|
377
|
+
try{
|
|
378
|
+
const pk = this.getPrimarykey(entity);
|
|
379
|
+
if(pk){
|
|
380
|
+
const hasPk = entitiesList.indexOf(pk) !== -1 || entitiesList.indexOf(`\`${pk}\``) !== -1;
|
|
381
|
+
if(!hasPk){ entitiesList.unshift(pk); }
|
|
382
|
+
}
|
|
383
|
+
}catch(_){ /* ignore */ }
|
|
310
384
|
return entitiesList
|
|
311
385
|
}
|
|
312
386
|
|
|
@@ -338,6 +412,27 @@ class SQLLiteEngine {
|
|
|
338
412
|
var dirtyFields = model.__dirtyFields;
|
|
339
413
|
|
|
340
414
|
for (var column in dirtyFields) {
|
|
415
|
+
// Validate non-nullable constraints on updates
|
|
416
|
+
var fieldName = dirtyFields[column];
|
|
417
|
+
var entityDef = model.__entity[fieldName];
|
|
418
|
+
if(entityDef && entityDef.nullable === false && entityDef.primary !== true){
|
|
419
|
+
// Determine the value that will actually be persisted for this field
|
|
420
|
+
var persistedValue;
|
|
421
|
+
switch(entityDef.type){
|
|
422
|
+
case "integer":
|
|
423
|
+
persistedValue = model["_" + fieldName];
|
|
424
|
+
break;
|
|
425
|
+
case "belongsTo":
|
|
426
|
+
persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName];
|
|
427
|
+
break;
|
|
428
|
+
default:
|
|
429
|
+
persistedValue = model[fieldName];
|
|
430
|
+
}
|
|
431
|
+
var isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
|
|
432
|
+
if(persistedValue === undefined || persistedValue === null || isEmptyString){
|
|
433
|
+
throw `Entity ${model.__entity.__name} column ${fieldName} is a required Field`;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
341
436
|
// TODO Boolean value is a string with a letter
|
|
342
437
|
var type = model.__entity[dirtyFields[column]].type;
|
|
343
438
|
|
|
@@ -347,14 +442,19 @@ class SQLLiteEngine {
|
|
|
347
442
|
|
|
348
443
|
switch(type){
|
|
349
444
|
case "belongsTo" :
|
|
350
|
-
|
|
445
|
+
const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
|
|
351
446
|
argument = `${foreignKey} = ${model[dirtyFields[column]]},`;
|
|
352
447
|
break;
|
|
353
448
|
case "integer" :
|
|
354
|
-
|
|
449
|
+
const columneValue = model[`_${dirtyFields[column]}`];
|
|
450
|
+
argument = argument === null ? `[${dirtyFields[column]}] = ${model[dirtyFields[column]]},` : `${argument} [${dirtyFields[column]}] = ${columneValue},`;
|
|
355
451
|
break;
|
|
356
452
|
case "string" :
|
|
357
|
-
argument = argument === null ? `${dirtyFields[column]} = '${$that._santizeSingleQuotes(model[dirtyFields[column]])}',` : `${argument} ${dirtyFields[column]} = '${$that._santizeSingleQuotes(model[dirtyFields[column]])}',`;
|
|
453
|
+
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] })}',`;
|
|
454
|
+
break;
|
|
455
|
+
case "time" :
|
|
456
|
+
// Always quote time values so empty strings remain valid ('')
|
|
457
|
+
argument = argument === null ? `${dirtyFields[column]} = '${model[dirtyFields[column]]}',` : `${argument} ${dirtyFields[column]} = '${model[dirtyFields[column]]}',`;
|
|
358
458
|
break;
|
|
359
459
|
case "boolean" :
|
|
360
460
|
argument = argument === null ? `${dirtyFields[column]} = '${this.boolType(model[dirtyFields[column]])}',` : `${argument} ${dirtyFields[column]} = '${this.boolType(model[dirtyFields[column]])}',`;
|
|
@@ -363,7 +463,12 @@ class SQLLiteEngine {
|
|
|
363
463
|
argument = argument === null ? `${dirtyFields[column]} = '${model[dirtyFields[column]]}',` : `${argument} ${dirtyFields[column]} = '${model[dirtyFields[column]]}',`;
|
|
364
464
|
}
|
|
365
465
|
}
|
|
366
|
-
|
|
466
|
+
if(argument){
|
|
467
|
+
return argument.replace(/,\s*$/, "");
|
|
468
|
+
}
|
|
469
|
+
else{
|
|
470
|
+
return -1;
|
|
471
|
+
}
|
|
367
472
|
}
|
|
368
473
|
|
|
369
474
|
boolType(type){
|
|
@@ -402,15 +507,14 @@ class SQLLiteEngine {
|
|
|
402
507
|
// check if get function is avaliable if so use that
|
|
403
508
|
fieldColumn = fields[column];
|
|
404
509
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if((fieldColumn !== undefined && fieldColumn !== null && fieldColumn !== "" ) && typeof(fieldColumn) !== "object"){
|
|
510
|
+
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
408
511
|
switch(modelEntity[column].type){
|
|
409
512
|
case "string" :
|
|
410
|
-
fieldColumn = `'${$that._santizeSingleQuotes(fields[column])}'`;
|
|
513
|
+
fieldColumn = `'${$that._santizeSingleQuotes(fields[column], { entityName: modelEntity.__name, fieldName: column })}'`;
|
|
411
514
|
break;
|
|
412
515
|
case "time" :
|
|
413
|
-
|
|
516
|
+
// Quote time values to prevent blank values from producing malformed SQL
|
|
517
|
+
fieldColumn = `'${fields[column]}'`;
|
|
414
518
|
break;
|
|
415
519
|
}
|
|
416
520
|
|
|
@@ -420,7 +524,8 @@ class SQLLiteEngine {
|
|
|
420
524
|
}
|
|
421
525
|
|
|
422
526
|
|
|
423
|
-
|
|
527
|
+
// Use backtick-quoted identifiers for MySQL column names
|
|
528
|
+
columns = columns === null ? `\`${column}\`,` : `${columns} \`${column}\`,`;
|
|
424
529
|
values = values === null ? `${fieldColumn},` : `${values} ${fieldColumn},`;
|
|
425
530
|
|
|
426
531
|
}
|
|
@@ -431,13 +536,23 @@ class SQLLiteEngine {
|
|
|
431
536
|
|
|
432
537
|
}
|
|
433
538
|
|
|
434
|
-
// will add double single quotes to allow
|
|
435
|
-
_santizeSingleQuotes(
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
539
|
+
// will add double single quotes to allow string to be saved.
|
|
540
|
+
_santizeSingleQuotes(value, context){
|
|
541
|
+
if (typeof value === 'string' || value instanceof String){
|
|
542
|
+
return value.replace(/'/g, "''");
|
|
543
|
+
}
|
|
544
|
+
else{
|
|
545
|
+
var details = context || {};
|
|
546
|
+
var entityName = details.entityName || 'UnknownEntity';
|
|
547
|
+
var fieldName = details.fieldName || 'UnknownField';
|
|
548
|
+
var valueType = (value === null) ? 'null' : (value === undefined ? 'undefined' : typeof value);
|
|
549
|
+
var preview;
|
|
550
|
+
try{ preview = (value === null || value === undefined) ? String(value) : JSON.stringify(value); }
|
|
551
|
+
catch(_){ preview = '[unserializable]'; }
|
|
552
|
+
if(preview && preview.length > 120){ preview = preview.substring(0, 120) + '…'; }
|
|
553
|
+
var message = `Field is not a string: entity=${entityName}, field=${fieldName}, type=${valueType}, value=${preview}`;
|
|
554
|
+
console.error(message);
|
|
555
|
+
throw new Error(message);
|
|
441
556
|
}
|
|
442
557
|
}
|
|
443
558
|
|
package/mySQLSyncConnect.js
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
3
|
"dependencies": {
|
|
4
|
-
"commander": "^
|
|
5
|
-
"glob" : "^11.0.
|
|
4
|
+
"commander": "^14.0.1",
|
|
5
|
+
"glob" : "^11.0.3",
|
|
6
6
|
"deep-object-diff" : "^1.1.9",
|
|
7
|
-
"pg" : "^8.
|
|
8
|
-
"sync-mysql2" : "^1.0.
|
|
7
|
+
"pg" : "^8.16.3",
|
|
8
|
+
"sync-mysql2" : "^1.0.6",
|
|
9
9
|
"app-root-path": "^3.1.0"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.1
|
|
11
|
+
"version": "0.2.1",
|
|
12
12
|
"description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
|
|
13
13
|
"homepage": "https://github.com/Tailor/MasterRecord#readme",
|
|
14
14
|
"repository": {
|
package/readme.md
CHANGED