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/mySQLEngine.js CHANGED
@@ -1,4 +1,5 @@
1
- // version : 0.0.1
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
- var whereEntity = query.where;
126
- var strQuery = "";
127
- var $that = this;
128
- if(whereEntity){
129
- var entity = this.getEntity(query.parentName, query.entityMap);
130
- for (let part in whereEntity[query.parentName]) {
131
- var item = whereEntity[query.parentName][part];
132
- for (let exp in item.expressions) {
133
- var field = tools.capitalizeFirstLetter(item.expressions[exp].field);
134
- if(mainQuery[field]){
135
- if(mainQuery[field].isNavigational){
136
- entity = $that.getEntity(field, query.entityMap);
137
- field = item.fields[1];
138
- }
139
- }
140
- if(strQuery === ""){
141
- strQuery = `WHERE ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
142
- }
143
- else{
144
- strQuery = `${strQuery} and ${entity}.${field} ${item.expressions[exp].func} '${item.expressions[exp].arg}'`;
145
- }
146
- }
147
- }
148
- }
149
- return strQuery;
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
- var foreignKey = model.__entity[dirtyFields[column]].foreignKey;
445
+ const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
351
446
  argument = `${foreignKey} = ${model[dirtyFields[column]]},`;
352
447
  break;
353
448
  case "integer" :
354
- argument = argument === null ? `${dirtyFields[column]} = ${model[dirtyFields[column]]},` : `${argument} ${dirtyFields[column]} = ${model[dirtyFields[column]]},`;
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
- return argument.replace(/,\s*$/, "");
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
- fieldColumn = fields[column];
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
- columns = columns === null ? `${column},` : `${columns} ${column},`;
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 sting to be saved.
435
- _santizeSingleQuotes(string){
436
-
437
- if(typeof string === "string"){
438
- return string.replace(/'/g, "''");
439
- }else{
440
- return `${string}`;
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
 
@@ -1,5 +1,6 @@
1
- var MySql = require('sync-mysql2');
1
+ // version : 0.0.1
2
2
 
3
+ var MySql = require('sync-mysql2');
3
4
 
4
5
  class MySQLClient {
5
6
  constructor(config) {
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "masterrecord",
3
3
  "dependencies": {
4
- "commander": "^13.1.0",
5
- "glob" : "^11.0.1",
4
+ "commander": "^14.0.1",
5
+ "glob" : "^11.0.3",
6
6
  "deep-object-diff" : "^1.1.9",
7
- "pg" : "^8.14.1",
8
- "sync-mysql2" : "^1.0.5",
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.4",
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
@@ -1 +1,3 @@
1
- local install of CLI package run code in terminal - "npm install -g ./" master
1
+ local install of CLI package run code in terminal - "npm install -g ./" master
2
+
3
+ For full instructions on running migrations and updating the server, see `MIGRATIONS.md`.