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/context.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Version 0.0.
|
|
1
|
+
// Version 0.0.15
|
|
2
2
|
|
|
3
3
|
var modelBuilder = require('./Entity/entityModelBuilder');
|
|
4
4
|
var query = require('masterrecord/QueryLanguage/queryMethods');
|
|
@@ -9,6 +9,7 @@ var insertManager = require('./insertManager');
|
|
|
9
9
|
var deleteManager = require('./deleteManager');
|
|
10
10
|
var globSearch = require("glob");
|
|
11
11
|
var fs = require('fs');
|
|
12
|
+
var path = require('path');
|
|
12
13
|
const appRoot = require('app-root-path');
|
|
13
14
|
const MySQLClient = require('masterrecord/mySQLSyncConnect');
|
|
14
15
|
|
|
@@ -54,6 +55,7 @@ class context {
|
|
|
54
55
|
}
|
|
55
56
|
catch (e) {
|
|
56
57
|
console.log("error SQL", e);
|
|
58
|
+
throw new Error(String(e))
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -90,74 +92,187 @@ class context {
|
|
|
90
92
|
};
|
|
91
93
|
|
|
92
94
|
__findSettings(root, rootFolderLocation, envType){
|
|
93
|
-
|
|
94
95
|
if(envType === undefined){
|
|
95
96
|
envType = "development";
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
98
|
+
let currentRoot = root;
|
|
99
|
+
const maxHops = 12;
|
|
100
|
+
for(let i = 0; i < maxHops; i++){
|
|
101
|
+
const rootFolder = path.isAbsolute(rootFolderLocation) ? rootFolderLocation : `${currentRoot}/${rootFolderLocation}`;
|
|
102
|
+
// Support both env.development.json and development.json naming
|
|
103
|
+
const searchA = `${rootFolder}/**/*env.${envType}.json`;
|
|
104
|
+
const searchB = `${rootFolder}/**/*${envType}.json`;
|
|
105
|
+
let files = globSearch.sync(searchA, currentRoot);
|
|
106
|
+
if(!files || files.length === 0){
|
|
107
|
+
files = globSearch.sync(searchB, currentRoot);
|
|
108
|
+
}
|
|
109
|
+
const file = files && files[0];
|
|
110
|
+
if(file){
|
|
111
|
+
return { file: file, rootFolder: currentRoot };
|
|
112
|
+
}
|
|
113
|
+
const parent = tools.removeBackwardSlashSection(currentRoot, 1, "/");
|
|
114
|
+
if(parent === currentRoot || parent === ""){
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
currentRoot = parent;
|
|
118
|
+
}
|
|
119
|
+
const msg = `could not find env file '${rootFolderLocation}/env.${envType}.json' starting at ${root}`;
|
|
120
|
+
console.log(msg);
|
|
121
|
+
throw new Error(msg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Auto-detect DB type (sqlite or mysql) using environment JSON
|
|
125
|
+
env(rootFolderLocation){
|
|
126
|
+
try{
|
|
127
|
+
// Determine environment: prefer explicit, then NODE_ENV, fallback 'development'
|
|
128
|
+
let envType = this.__environment || process.env.NODE_ENV || 'development';
|
|
129
|
+
const contextName = this.__name;
|
|
130
|
+
|
|
131
|
+
// Try multiple base roots for robustness
|
|
132
|
+
const candidateRoots = [ process.cwd(), appRoot.path, __dirname ];
|
|
133
|
+
let file;
|
|
134
|
+
for(let i = 0; i < candidateRoots.length; i++){
|
|
135
|
+
try{
|
|
136
|
+
file = this.__findSettings(candidateRoots[i], rootFolderLocation, envType);
|
|
137
|
+
if(file) break;
|
|
138
|
+
}catch(_){ /* try next */ }
|
|
139
|
+
}
|
|
140
|
+
// If still not found and an absolute path was provided, try directly
|
|
141
|
+
if(!file && path.isAbsolute(rootFolderLocation)){
|
|
142
|
+
const directFolder = rootFolderLocation;
|
|
143
|
+
const envFileA = path.join(directFolder, `env.${envType}.json`);
|
|
144
|
+
const envFileB = path.join(directFolder, `${envType}.json`);
|
|
145
|
+
const picked = fs.existsSync(envFileA) ? envFileA : (fs.existsSync(envFileB) ? envFileB : null);
|
|
146
|
+
if(picked){
|
|
147
|
+
file = { file: picked, rootFolder: path.dirname(path.dirname(picked)) };
|
|
116
148
|
}
|
|
149
|
+
}
|
|
150
|
+
if(!file){
|
|
151
|
+
throw new Error(`Environment config not found for '${envType}' under '${rootFolderLocation}'.`);
|
|
152
|
+
}
|
|
117
153
|
|
|
154
|
+
const settings = require(file.file);
|
|
155
|
+
const options = settings[contextName];
|
|
156
|
+
if(options === undefined){
|
|
157
|
+
console.log("settings missing context name settings");
|
|
158
|
+
throw new Error("settings missing context name settings");
|
|
118
159
|
}
|
|
119
|
-
|
|
160
|
+
|
|
161
|
+
const type = String(options.type || '').toLowerCase();
|
|
162
|
+
|
|
163
|
+
if(type === 'sqlite' || type === 'better-sqlite3'){
|
|
164
|
+
this.isSQLite = true; this.isMySQL = false;
|
|
165
|
+
// Back-compat: treat leading '/' as project-root relative, not filesystem root
|
|
166
|
+
let dbPath = options.connection || '';
|
|
167
|
+
if(dbPath){
|
|
168
|
+
if(dbPath.startsWith(path.sep) || !path.isAbsolute(dbPath)){
|
|
169
|
+
dbPath = path.join(file.rootFolder, dbPath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const dbDir = path.dirname(dbPath);
|
|
173
|
+
if(!fs.existsSync(dbDir)){
|
|
174
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
const sqliteOptions = { ...options, completeConnection: dbPath };
|
|
177
|
+
this.db = this.__SQLiteInit(sqliteOptions, 'better-sqlite3');
|
|
178
|
+
this._SQLEngine.setDB(this.db, 'better-sqlite3');
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if(type === 'mysql'){
|
|
183
|
+
this.isMySQL = true; this.isSQLite = false;
|
|
184
|
+
this.db = this.__mysqlInit(options, 'mysql2');
|
|
185
|
+
this._SQLEngine.setDB(this.db, 'mysql');
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new Error(`Unsupported database type '${options.type}'. Expected 'sqlite' or 'mysql'.`);
|
|
190
|
+
}
|
|
191
|
+
catch(err){
|
|
192
|
+
console.log("error:", err);
|
|
193
|
+
throw new Error(String(err));
|
|
120
194
|
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
file: file,
|
|
124
|
-
rootFolder : root
|
|
125
|
-
};
|
|
126
195
|
}
|
|
127
196
|
|
|
128
197
|
useSqlite(rootFolderLocation){
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
198
|
+
try{
|
|
199
|
+
this.isSQLite = true;
|
|
200
|
+
var root = process.cwd();
|
|
201
|
+
var envType = this.__environment;
|
|
202
|
+
var contextName = this.__name;
|
|
203
|
+
var file = this.__findSettings(root, rootFolderLocation, envType);
|
|
204
|
+
var settings = require(file.file);
|
|
205
|
+
var options = settings[contextName];
|
|
206
|
+
|
|
207
|
+
if(options === undefined){
|
|
208
|
+
console.log("settings missing context name settings");
|
|
209
|
+
throw new Error("settings missing context name settings");
|
|
210
|
+
}
|
|
141
211
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
212
|
+
this.validateSQLiteOptions(options);
|
|
213
|
+
options.completeConnection = `${file.rootFolder}${options.connection}`;
|
|
214
|
+
var dbDirectory = options.completeConnection.substr(0, options.completeConnection.lastIndexOf("\/"));
|
|
215
|
+
|
|
216
|
+
if (!fs.existsSync(dbDirectory)){
|
|
217
|
+
fs.mkdirSync(dbDirectory);
|
|
218
|
+
}
|
|
149
219
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
220
|
+
this.db = this.__SQLiteInit(options, "better-sqlite3");
|
|
221
|
+
this._SQLEngine.setDB(this.db, "better-sqlite3");
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
224
|
+
catch(err){
|
|
225
|
+
console.log("error:",err );
|
|
226
|
+
throw new Error(String(err));
|
|
227
|
+
}
|
|
153
228
|
}
|
|
154
229
|
|
|
155
230
|
validateSQLiteOptions(options){
|
|
156
|
-
if(options
|
|
157
|
-
|
|
158
|
-
|
|
231
|
+
if(!options || typeof options !== 'object'){
|
|
232
|
+
throw new Error("settings object is missing or invalid");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Normalize type
|
|
236
|
+
let type = (options.type || '').toString().toLowerCase();
|
|
237
|
+
if(!type){
|
|
238
|
+
// Infer when not provided
|
|
239
|
+
if(typeof options.connection === 'string'){
|
|
240
|
+
type = 'sqlite';
|
|
241
|
+
options.type = 'sqlite';
|
|
242
|
+
}
|
|
243
|
+
else if(options.host || options.user || options.database){
|
|
244
|
+
type = 'mysql';
|
|
245
|
+
options.type = 'mysql';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if(type === 'sqlite' || type === 'better-sqlite3'){
|
|
250
|
+
// Required
|
|
251
|
+
if(!options.connection || typeof options.connection !== 'string' || options.connection.trim() === ''){
|
|
252
|
+
throw new Error("connection string settings is missing");
|
|
253
|
+
}
|
|
254
|
+
// Defaults
|
|
255
|
+
if(options.username === undefined){ options.username = ''; }
|
|
256
|
+
if(options.password === undefined){ options.password = ''; }
|
|
257
|
+
return; // valid
|
|
159
258
|
}
|
|
160
259
|
|
|
260
|
+
if(type === 'mysql'){
|
|
261
|
+
// Defaults
|
|
262
|
+
if(!options.host){ options.host = 'localhost'; }
|
|
263
|
+
if(options.port === undefined){ options.port = 3306; }
|
|
264
|
+
if(options.password === undefined){ options.password = ''; }
|
|
265
|
+
// Required
|
|
266
|
+
if(!options.user || options.user.toString().trim() === ''){
|
|
267
|
+
throw new Error("MySQL 'user' is required in settings");
|
|
268
|
+
}
|
|
269
|
+
if(!options.database || options.database.toString().trim() === ''){
|
|
270
|
+
throw new Error("MySQL 'database' is required in settings");
|
|
271
|
+
}
|
|
272
|
+
return; // valid
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error(`Unsupported database type '${options.type}'. Expected 'sqlite' or 'mysql'.`);
|
|
161
276
|
}
|
|
162
277
|
|
|
163
278
|
useMySql(rootFolderLocation){
|
|
@@ -172,9 +287,10 @@ class context {
|
|
|
172
287
|
|
|
173
288
|
if(options === undefined){
|
|
174
289
|
console.log("settings missing context name settings");
|
|
175
|
-
throw
|
|
290
|
+
throw new Error("settings missing context name settings");
|
|
176
291
|
}
|
|
177
292
|
|
|
293
|
+
this.validateSQLiteOptions(options);
|
|
178
294
|
this.db = this.__mysqlInit(options, "mysql2");
|
|
179
295
|
this._SQLEngine.setDB(this.db, "mysql");
|
|
180
296
|
return this;
|
|
@@ -213,12 +329,18 @@ class context {
|
|
|
213
329
|
break;
|
|
214
330
|
case "modified":
|
|
215
331
|
if(currentModel.__dirtyFields.length > 0){
|
|
216
|
-
var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
|
|
332
|
+
var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
|
|
217
333
|
// build columns equal to value string
|
|
218
334
|
var argu = this._SQLEngine._buildSQLEqualTo(cleanCurrentModel);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
335
|
+
if(argu !== -1 ){
|
|
336
|
+
var primaryKey = tools.getPrimaryKeyObject(cleanCurrentModel.__entity);
|
|
337
|
+
var sqlUpdate = {tableName: cleanCurrentModel.__entity.__name, arg: argu, primaryKey : primaryKey, primaryKeyValue : cleanCurrentModel[primaryKey] };
|
|
338
|
+
this._SQLEngine.update(sqlUpdate);
|
|
339
|
+
}
|
|
340
|
+
else{
|
|
341
|
+
console.log("Nothing has been tracked, modified, created or added");
|
|
342
|
+
}
|
|
343
|
+
|
|
222
344
|
}
|
|
223
345
|
else{
|
|
224
346
|
console.log("Tracked entity modified with no values being changed");
|
|
@@ -251,9 +373,15 @@ class context {
|
|
|
251
373
|
var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
|
|
252
374
|
// build columns equal to value string
|
|
253
375
|
var argu = this._SQLEngine._buildSQLEqualTo(cleanCurrentModel);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
376
|
+
if(argu !== -1 ){
|
|
377
|
+
var primaryKey = tools.getPrimaryKeyObject(cleanCurrentModel.__entity);
|
|
378
|
+
var sqlUpdate = {tableName: cleanCurrentModel.__entity.__name, arg: argu, primaryKey : primaryKey, primaryKeyValue : cleanCurrentModel[primaryKey] };
|
|
379
|
+
this._SQLEngine.update(sqlUpdate);
|
|
380
|
+
}
|
|
381
|
+
else{
|
|
382
|
+
console.log("Nothing has been tracked, modified, created or added");
|
|
383
|
+
}
|
|
384
|
+
|
|
257
385
|
}
|
|
258
386
|
else{
|
|
259
387
|
console.log("Tracked entity modified with no values being changed");
|
|
@@ -306,6 +434,9 @@ class context {
|
|
|
306
434
|
var add = true;
|
|
307
435
|
for (var mod in this.__trackedEntities) {
|
|
308
436
|
var id = this.__trackedEntities[mod].__ID;
|
|
437
|
+
if(id === undefined){
|
|
438
|
+
id = Math.floor((Math.random() * 100000) + 1);
|
|
439
|
+
}
|
|
309
440
|
if(id === model.__ID){
|
|
310
441
|
add = false;
|
|
311
442
|
}
|
package/deleteManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// version 0.0.
|
|
1
|
+
// version 0.0.3
|
|
2
2
|
var tools = require('./Tools');
|
|
3
3
|
class DeleteManager{
|
|
4
4
|
constructor(sqlEngine, entities){
|
|
@@ -23,7 +23,7 @@ class DeleteManager{
|
|
|
23
23
|
// loop though all entity properties
|
|
24
24
|
for (const property of entityKeys) {
|
|
25
25
|
// cascade delete
|
|
26
|
-
if(currentModel.__entity[property].type === "hasOne" || currentModel.__entity[property].type === "hasMany"){
|
|
26
|
+
if(currentModel.__entity[property].type === "hasOne" || currentModel.__entity[property].type === "hasMany" || currentModel.__entity[property].type === "hasManyThrough"){
|
|
27
27
|
var curModel = currentModel[property];
|
|
28
28
|
if(curModel === null){
|
|
29
29
|
// check if state is nullable - if so and nothing comes back dont call cascadeDelete
|
|
@@ -46,7 +46,7 @@ class DeleteManager{
|
|
|
46
46
|
// loop though all entity properties
|
|
47
47
|
for (const property of entityKeys) {
|
|
48
48
|
// cascade delete
|
|
49
|
-
if(currentModel[i].__entity[property].type === "hasOne" || currentModel[i].__entity[property].type === "hasMany"){
|
|
49
|
+
if(currentModel[i].__entity[property].type === "hasOne" || currentModel[i].__entity[property].type === "hasMany" || currentModel[i].__entity[property].type === "hasManyThrough"){
|
|
50
50
|
$that.cascadeDelete( currentModel[i][property]);
|
|
51
51
|
}
|
|
52
52
|
}
|
package/insertManager.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
// version
|
|
2
|
+
// version 0.0.15
|
|
3
3
|
var tools = require('./Tools');
|
|
4
4
|
var queryScript = require('masterrecord/QueryLanguage/queryScript');
|
|
5
5
|
|
|
@@ -18,45 +18,61 @@ class InsertManager {
|
|
|
18
18
|
|
|
19
19
|
runQueries(currentModel){
|
|
20
20
|
var $that = this;
|
|
21
|
-
|
|
21
|
+
// Reset validation state for this operation to avoid stale errors
|
|
22
|
+
if(this._errorModel){
|
|
23
|
+
this._errorModel.isValid = true;
|
|
24
|
+
this._errorModel.errors = [];
|
|
25
|
+
}
|
|
26
|
+
var cleanCurrentModel = tools.clearAllProto(currentModel);
|
|
22
27
|
this.validateEntity(cleanCurrentModel, currentModel, currentModel.__entity);
|
|
23
28
|
if(this._errorModel.isValid){
|
|
24
29
|
|
|
25
30
|
var modelEntity = currentModel.__entity;
|
|
26
31
|
// TODO: if you try to add belongs to you must have a tag added first. if you dont throw error
|
|
27
32
|
currentModel = this.belongsToInsert(currentModel, modelEntity);
|
|
28
|
-
var SQL = this._SQLEngine.insert(
|
|
33
|
+
var SQL = this._SQLEngine.insert(cleanCurrentModel);
|
|
29
34
|
var primaryKey = tools.getPrimaryKeyObject(currentModel.__entity);
|
|
30
|
-
//
|
|
35
|
+
// use returned insert id directly; avoid redundant post-insert SELECT
|
|
31
36
|
if(currentModel.__entity[primaryKey].auto === true){
|
|
32
|
-
|
|
33
|
-
var jj = this.__queryObject.raw(query);
|
|
34
|
-
var getQueryModel = this._SQLEngine.get(jj, currentModel.__entity, currentModel.__context );
|
|
35
|
-
var idVal;
|
|
36
|
-
|
|
37
|
-
if(!getQueryModel[0]){
|
|
38
|
-
idVal = getQueryModel[primaryKey]
|
|
39
|
-
}
|
|
40
|
-
else{
|
|
41
|
-
idVal = getQueryModel[0][primaryKey];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
currentModel[primaryKey] = idVal;
|
|
37
|
+
currentModel[primaryKey] = SQL.id;
|
|
45
38
|
}
|
|
46
39
|
|
|
40
|
+
const proto = Object.getPrototypeOf(currentModel);
|
|
41
|
+
const props = Object.getOwnPropertyNames(proto);
|
|
42
|
+
const cleanPropList = tools.returnEntityList(props, modelEntity);
|
|
47
43
|
const modelKeys = Object.keys(currentModel);
|
|
44
|
+
const mergedArray = [...new Set(modelKeys.concat(cleanPropList))];
|
|
48
45
|
// loop through model properties
|
|
49
|
-
for (const property of
|
|
46
|
+
for (const property of mergedArray) {
|
|
50
47
|
var propertyModel = currentModel[property];
|
|
51
48
|
var entityProperty = modelEntity[property] ? modelEntity[property] : {};
|
|
52
49
|
if(entityProperty.type === "hasOne"){
|
|
53
|
-
//
|
|
54
|
-
if(typeof(propertyModel) === "object"){
|
|
50
|
+
// only insert child if user provided a concrete object with data
|
|
51
|
+
if(propertyModel && typeof(propertyModel) === "object" ){
|
|
55
52
|
// check if model has its own entity
|
|
56
53
|
if(modelEntity){
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
// check if property has a value because we dont want this to run on every insert if nothing was added
|
|
55
|
+
// ensure it has some own props; otherwise skip
|
|
56
|
+
const hasOwn = Object.keys(propertyModel).length > 0;
|
|
57
|
+
if(hasOwn){
|
|
58
|
+
propertyModel.__entity = tools.getEntity(property, $that._allEntities);
|
|
59
|
+
propertyModel[currentModel.__entity.__name] = SQL.id;
|
|
60
|
+
$that.runQueries(propertyModel);
|
|
61
|
+
// Hydrate child back as a tracked instance so subsequent property sets are tracked
|
|
62
|
+
try{
|
|
63
|
+
var childPk = tools.getPrimaryKeyObject(propertyModel.__entity);
|
|
64
|
+
var childId = propertyModel[childPk];
|
|
65
|
+
if(childId !== undefined){
|
|
66
|
+
var ctxSetName = tools.capitalizeFirstLetter(property);
|
|
67
|
+
if(currentModel.__context && currentModel.__context[ctxSetName]){
|
|
68
|
+
var trackedChild = currentModel.__context[ctxSetName].where(`r => r.${childPk} == $$`, childId).single();
|
|
69
|
+
if(trackedChild){
|
|
70
|
+
currentModel[property] = trackedChild;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}catch(_){ /* ignore tracking hydration errors */ }
|
|
75
|
+
}
|
|
60
76
|
}
|
|
61
77
|
else{
|
|
62
78
|
throw `Relationship "${entityProperty.name}" could not be found please check if object has correct spelling or if it has been added to the context class`
|
|
@@ -65,11 +81,58 @@ class InsertManager {
|
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
if(entityProperty.type === "hasMany"){
|
|
68
|
-
if
|
|
84
|
+
// skip when not provided; only enforce array type if user supplied a value
|
|
85
|
+
if(propertyModel === undefined || propertyModel === null){
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if(tools.checkIfArrayLike(propertyModel)){
|
|
69
89
|
const propertyKeys = Object.keys(propertyModel);
|
|
70
90
|
for (const propertykey of propertyKeys) {
|
|
71
91
|
if(propertyModel[propertykey]){
|
|
72
|
-
|
|
92
|
+
let targetName = entityProperty.foreignTable || property;
|
|
93
|
+
let resolved = tools.getEntity(targetName, $that._allEntities)
|
|
94
|
+
|| tools.getEntity(tools.capitalize(targetName), $that._allEntities)
|
|
95
|
+
|| tools.getEntity(property, $that._allEntities);
|
|
96
|
+
if(!resolved){
|
|
97
|
+
throw `Relationship entity for '${property}' could not be resolved. Expected '${targetName}'.`;
|
|
98
|
+
}
|
|
99
|
+
// Coerce primitive into object with primary key if user passed an id
|
|
100
|
+
if(typeof propertyModel[propertykey] !== "object" || propertyModel[propertykey] === null){
|
|
101
|
+
const childPrimaryKey = tools.getPrimaryKeyObject(resolved);
|
|
102
|
+
const primitiveValue = propertyModel[propertykey];
|
|
103
|
+
propertyModel[propertykey] = {};
|
|
104
|
+
propertyModel[propertykey][childPrimaryKey] = primitiveValue;
|
|
105
|
+
}
|
|
106
|
+
propertyModel[propertykey].__entity = resolved;
|
|
107
|
+
propertyModel[propertykey][currentModel.__entity.__name] = SQL.id;
|
|
108
|
+
$that.runQueries(propertyModel[propertykey]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else{
|
|
113
|
+
throw `Relationship "${entityProperty.name}" must be an array`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if(entityProperty.type === "hasManyThrough"){
|
|
118
|
+
if(tools.checkIfArrayLike(propertyModel)){
|
|
119
|
+
const propertyKeys = Object.keys(propertyModel);
|
|
120
|
+
for (const propertykey of propertyKeys) {
|
|
121
|
+
if(propertyModel[propertykey]){
|
|
122
|
+
let targetName = entityProperty.foreignTable || property;
|
|
123
|
+
let resolved = tools.getEntity(targetName, $that._allEntities)
|
|
124
|
+
|| tools.getEntity(tools.capitalize(targetName), $that._allEntities)
|
|
125
|
+
|| tools.getEntity(property, $that._allEntities);
|
|
126
|
+
if(!resolved){
|
|
127
|
+
throw `Relationship entity for '${property}' could not be resolved. Expected '${targetName}'.`;
|
|
128
|
+
}
|
|
129
|
+
if(typeof propertyModel[propertykey] !== "object" || propertyModel[propertykey] === null){
|
|
130
|
+
const childPrimaryKey = tools.getPrimaryKeyObject(resolved);
|
|
131
|
+
const primitiveValue = propertyModel[propertykey];
|
|
132
|
+
propertyModel[propertykey] = {};
|
|
133
|
+
propertyModel[propertykey][childPrimaryKey] = primitiveValue;
|
|
134
|
+
}
|
|
135
|
+
propertyModel[propertykey].__entity = resolved;
|
|
73
136
|
propertyModel[propertykey][currentModel.__entity.__name] = SQL.id;
|
|
74
137
|
$that.runQueries(propertyModel[propertykey]);
|
|
75
138
|
}
|
|
@@ -83,11 +146,15 @@ class InsertManager {
|
|
|
83
146
|
}
|
|
84
147
|
}
|
|
85
148
|
else{
|
|
86
|
-
var
|
|
87
|
-
|
|
149
|
+
var messages = this._errorModel.errors;
|
|
150
|
+
const combinedError = messages.join('; and ');
|
|
151
|
+
throw combinedError;
|
|
152
|
+
|
|
88
153
|
}
|
|
89
154
|
}
|
|
90
155
|
|
|
156
|
+
|
|
157
|
+
|
|
91
158
|
// will insert belongs to row first and return the id so that next call can be make correctly
|
|
92
159
|
belongsToInsert(currentModel, modelEntity){
|
|
93
160
|
var $that = this;
|
|
@@ -98,7 +165,7 @@ class InsertManager {
|
|
|
98
165
|
// check if model is a an object. If so insert the child first then the parent.
|
|
99
166
|
if(typeof newPropertyModel === 'object'){
|
|
100
167
|
newPropertyModel.__entity = tools.getEntity(entity, $that._allEntities);
|
|
101
|
-
var propertyCleanCurrentModel = tools.
|
|
168
|
+
var propertyCleanCurrentModel = tools.clearAllProto(newPropertyModel);
|
|
102
169
|
this.validateEntity(propertyCleanCurrentModel, newPropertyModel, newPropertyModel.__entity);
|
|
103
170
|
var propertySQL = this._SQLEngine.insert(newPropertyModel);
|
|
104
171
|
currentModel[foreignKey] = propertySQL.id;
|
|
@@ -111,35 +178,62 @@ class InsertManager {
|
|
|
111
178
|
// update the currentModel.
|
|
112
179
|
return currentModel;
|
|
113
180
|
}
|
|
114
|
-
|
|
181
|
+
|
|
182
|
+
// validate entity for nullable fields and if the entity has any values at all
|
|
115
183
|
validateEntity(currentModel, currentRealModel, entityModel){
|
|
116
184
|
for(var entity in entityModel) {
|
|
117
185
|
var currentEntity = entityModel[entity];
|
|
118
186
|
if (entityModel.hasOwnProperty(entity)) {
|
|
119
187
|
// check if there is a default value
|
|
120
188
|
if(currentEntity.default){
|
|
121
|
-
if(
|
|
189
|
+
if(currentRealModel[entity] === undefined || currentRealModel[entity] === null){
|
|
122
190
|
// if its empty add the default value
|
|
123
191
|
currentRealModel[entity] = currentEntity.default;
|
|
124
192
|
}
|
|
125
193
|
}
|
|
126
194
|
|
|
127
|
-
// SKIP belongs too
|
|
195
|
+
// SKIP belongs too ----- // call sets for correct data for DB
|
|
128
196
|
if(currentEntity.type !== "belongsTo" && currentEntity.type !== "hasMany"){
|
|
129
197
|
if(currentEntity.relationshipType !== "belongsTo"){
|
|
198
|
+
// Auto-populate common timestamp fields if required and missing
|
|
199
|
+
if((entity === 'created_at' || entity === 'updated_at') && (currentRealModel[entity] === undefined || currentRealModel[entity] === null)){
|
|
200
|
+
var nowVal = Date.now().toString();
|
|
201
|
+
currentRealModel[entity] = nowVal;
|
|
202
|
+
currentModel[entity] = nowVal;
|
|
203
|
+
}
|
|
130
204
|
// primary is always null in an insert so validation insert must be null
|
|
131
205
|
if(currentEntity.nullable === false && !currentEntity.primary){
|
|
132
206
|
// if it doesnt have a get method then call error
|
|
133
207
|
if(currentEntity.set === undefined){
|
|
134
|
-
|
|
208
|
+
const realVal = currentRealModel[entity];
|
|
209
|
+
const cleanVal = currentModel[entity];
|
|
210
|
+
let hasValue = (realVal !== undefined && realVal !== null) || (cleanVal !== undefined && cleanVal !== null);
|
|
211
|
+
// For strings, empty string should be considered invalid for notNullable
|
|
212
|
+
const candidate = (realVal !== undefined && realVal !== null) ? realVal : cleanVal;
|
|
213
|
+
if(typeof candidate === 'string' && candidate.trim() === ''){
|
|
214
|
+
hasValue = false;
|
|
215
|
+
}
|
|
216
|
+
// Fallback: check backing field on tracked model if both reads were undefined/null
|
|
217
|
+
if(!hasValue && currentRealModel && currentRealModel.__proto__){
|
|
218
|
+
const backing = currentRealModel.__proto__["_" + entity];
|
|
219
|
+
hasValue = (backing !== undefined && backing !== null);
|
|
220
|
+
if(hasValue){
|
|
221
|
+
// normalize into both models so downstream sees it
|
|
222
|
+
currentRealModel[entity] = backing;
|
|
223
|
+
currentModel[entity] = backing;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if(!hasValue){
|
|
135
227
|
this._errorModel.isValid = false;
|
|
136
228
|
var errorMessage = `Entity ${currentModel.__entity.__name} column ${entity} is a required Field`;
|
|
137
229
|
this._errorModel.errors.push(errorMessage);
|
|
138
|
-
throw errorMessage;
|
|
230
|
+
//throw errorMessage;
|
|
139
231
|
}
|
|
140
232
|
}
|
|
141
233
|
else{
|
|
142
|
-
|
|
234
|
+
var realData = currentEntity.set(currentModel[entity]);
|
|
235
|
+
currentRealModel[entity] = realData;
|
|
236
|
+
currentModel[entity] = realData;
|
|
143
237
|
}
|
|
144
238
|
}
|
|
145
239
|
}
|