masterrecord 0.3.27 → 0.3.30
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/.claude/settings.local.json +21 -1
- package/Entity/entityModel.js +145 -1
- package/Entity/entityModelBuilder.js +21 -3
- package/Entity/entityTrackerModel.js +251 -1
- package/Migrations/migrationMySQLQuery.js +14 -0
- package/Migrations/migrationPostgresQuery.js +14 -0
- package/Migrations/migrationSQLiteQuery.js +14 -0
- package/Migrations/migrationTemplate.js +38 -0
- package/Migrations/migrations.js +107 -3
- package/Migrations/schema.js +66 -0
- package/QueryLanguage/queryMethods.js +330 -4
- package/SQLLiteEngine.js +4 -0
- package/Tools.js +15 -2
- package/context.js +198 -5
- package/mySQLEngine.js +11 -1
- package/package.json +1 -1
- package/postgresEngine.js +6 -1
- package/readme.md +1125 -8
- package/test/bulk-operations-test.js +235 -0
- package/test/cache-toObject-test.js +105 -0
- package/test/debug-id-test.js +63 -0
- package/test/double-where-bug-test.js +71 -0
- package/test/entity-methods-test.js +269 -0
- package/test/id-setting-validation.js +202 -0
- package/test/insert-return-test.js +39 -0
- package/test/lifecycle-hooks-test.js +258 -0
- package/test/query-helpers-test.js +258 -0
- package/test/query-isolation-test.js +59 -0
- package/test/simple-id-test.js +61 -0
- package/test/single-user-id-test.js +70 -0
- package/test/validation-test.js +302 -0
package/Migrations/migrations.js
CHANGED
|
@@ -24,7 +24,9 @@ class Migrations{
|
|
|
24
24
|
newColumns : [],
|
|
25
25
|
newTables : [],
|
|
26
26
|
deletedColumns : [],
|
|
27
|
-
updatedColumns : []
|
|
27
|
+
updatedColumns : [],
|
|
28
|
+
newIndexes : [],
|
|
29
|
+
deletedIndexes : []
|
|
28
30
|
}
|
|
29
31
|
tables.push(table);
|
|
30
32
|
});
|
|
@@ -38,7 +40,9 @@ class Migrations{
|
|
|
38
40
|
newColumns : [],
|
|
39
41
|
newTables : [],
|
|
40
42
|
deletedColumns : [],
|
|
41
|
-
updatedColumns : []
|
|
43
|
+
updatedColumns : [],
|
|
44
|
+
newIndexes : [],
|
|
45
|
+
deletedIndexes : []
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
oldSchema.forEach(function (oldItem, index) {
|
|
@@ -156,11 +160,101 @@ class Migrations{
|
|
|
156
160
|
#buildMigrationObject(oldSchema, newSchema){
|
|
157
161
|
|
|
158
162
|
var tables = this.#organizeSchemaByTables(oldSchema, newSchema);
|
|
159
|
-
|
|
163
|
+
|
|
160
164
|
tables = this.#findNewTables(tables);
|
|
161
165
|
tables = this.#findNewColumns(tables);
|
|
162
166
|
tables = this.#findDeletedColumns(tables);
|
|
163
167
|
tables = this.#findUpdatedColumns(tables);
|
|
168
|
+
tables = this.#findNewIndexes(tables);
|
|
169
|
+
tables = this.#findDeletedIndexes(tables);
|
|
170
|
+
return tables;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#findNewIndexes(tables){
|
|
174
|
+
tables.forEach(function (item, index) {
|
|
175
|
+
if(item.new && item.old){
|
|
176
|
+
Object.keys(item.new).forEach(function (key) {
|
|
177
|
+
if(typeof item.new[key] === "object" && item.new[key].indexes){
|
|
178
|
+
var columnName = item.new[key].name;
|
|
179
|
+
var newIndexes = item.new[key].indexes;
|
|
180
|
+
|
|
181
|
+
// Check if this column existed before
|
|
182
|
+
var oldColumn = null;
|
|
183
|
+
Object.keys(item.old).forEach(function (oldKey) {
|
|
184
|
+
if(typeof item.old[oldKey] === "object" && item.old[oldKey].name === columnName){
|
|
185
|
+
oldColumn = item.old[oldKey];
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// If column didn't exist before, or didn't have indexes, all indexes are new
|
|
190
|
+
if(!oldColumn || !oldColumn.indexes){
|
|
191
|
+
newIndexes.forEach(function(indexName){
|
|
192
|
+
item.newIndexes.push({
|
|
193
|
+
tableName: item.name,
|
|
194
|
+
columnName: columnName,
|
|
195
|
+
indexName: indexName
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
// Check for new indexes that weren't in the old column
|
|
200
|
+
newIndexes.forEach(function(indexName){
|
|
201
|
+
if(!oldColumn.indexes.includes(indexName)){
|
|
202
|
+
item.newIndexes.push({
|
|
203
|
+
tableName: item.name,
|
|
204
|
+
columnName: columnName,
|
|
205
|
+
indexName: indexName
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return tables;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#findDeletedIndexes(tables){
|
|
218
|
+
tables.forEach(function (item, index) {
|
|
219
|
+
if(item.new && item.old){
|
|
220
|
+
Object.keys(item.old).forEach(function (key) {
|
|
221
|
+
if(typeof item.old[key] === "object" && item.old[key].indexes){
|
|
222
|
+
var columnName = item.old[key].name;
|
|
223
|
+
var oldIndexes = item.old[key].indexes;
|
|
224
|
+
|
|
225
|
+
// Check if this column still exists
|
|
226
|
+
var newColumn = null;
|
|
227
|
+
Object.keys(item.new).forEach(function (newKey) {
|
|
228
|
+
if(typeof item.new[newKey] === "object" && item.new[newKey].name === columnName){
|
|
229
|
+
newColumn = item.new[newKey];
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// If column doesn't exist anymore, or doesn't have indexes, all indexes are deleted
|
|
234
|
+
if(!newColumn || !newColumn.indexes){
|
|
235
|
+
oldIndexes.forEach(function(indexName){
|
|
236
|
+
item.deletedIndexes.push({
|
|
237
|
+
tableName: item.name,
|
|
238
|
+
columnName: columnName,
|
|
239
|
+
indexName: indexName
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
// Check for indexes that were removed
|
|
244
|
+
oldIndexes.forEach(function(indexName){
|
|
245
|
+
if(!newColumn.indexes.includes(indexName)){
|
|
246
|
+
item.deletedIndexes.push({
|
|
247
|
+
tableName: item.name,
|
|
248
|
+
columnName: columnName,
|
|
249
|
+
indexName: indexName
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
});
|
|
164
258
|
return tables;
|
|
165
259
|
}
|
|
166
260
|
|
|
@@ -302,6 +396,8 @@ class Migrations{
|
|
|
302
396
|
(t.newColumns && t.newColumns.length) ||
|
|
303
397
|
(t.deletedColumns && t.deletedColumns.length) ||
|
|
304
398
|
(t.updatedColumns && t.updatedColumns.length) ||
|
|
399
|
+
(t.newIndexes && t.newIndexes.length) ||
|
|
400
|
+
(t.deletedIndexes && t.deletedIndexes.length) ||
|
|
305
401
|
(t.old === null) || (t.new === null)){
|
|
306
402
|
return true;
|
|
307
403
|
}
|
|
@@ -348,6 +444,14 @@ class Migrations{
|
|
|
348
444
|
}
|
|
349
445
|
});
|
|
350
446
|
|
|
447
|
+
item.newIndexes.forEach(function (indexInfo, index) {
|
|
448
|
+
MT.createIndex("up", indexInfo);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
item.deletedIndexes.forEach(function (indexInfo, index) {
|
|
452
|
+
MT.dropIndex("up", indexInfo);
|
|
453
|
+
});
|
|
454
|
+
|
|
351
455
|
});
|
|
352
456
|
|
|
353
457
|
return MT.get();
|
package/Migrations/schema.js
CHANGED
|
@@ -110,6 +110,22 @@ class schema{
|
|
|
110
110
|
var query = queryBuilder.createTable(table);
|
|
111
111
|
this.context._execute(query);
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
// Create indexes for columns that have .index() defined
|
|
115
|
+
const self = this;
|
|
116
|
+
Object.keys(table).forEach(function(key){
|
|
117
|
+
if(typeof table[key] === "object" && table[key].indexes && !key.startsWith('__')){
|
|
118
|
+
const columnName = table[key].name;
|
|
119
|
+
table[key].indexes.forEach(function(indexName){
|
|
120
|
+
const indexInfo = {
|
|
121
|
+
tableName: tableName,
|
|
122
|
+
columnName: columnName,
|
|
123
|
+
indexName: indexName
|
|
124
|
+
};
|
|
125
|
+
self.createIndex(indexInfo);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
113
129
|
}
|
|
114
130
|
}else{
|
|
115
131
|
console.log("Table that you're trying to create is undefined. Please check if there are any changes that need to be made");
|
|
@@ -394,6 +410,56 @@ class schema{
|
|
|
394
410
|
}
|
|
395
411
|
}
|
|
396
412
|
|
|
413
|
+
createIndex(indexInfo){
|
|
414
|
+
if(indexInfo){
|
|
415
|
+
if(this.context.isSQLite){
|
|
416
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
417
|
+
var queryBuilder = new sqliteQuery();
|
|
418
|
+
var query = queryBuilder.createIndex(indexInfo);
|
|
419
|
+
this.context._execute(query);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if(this.context.isMySQL){
|
|
423
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
424
|
+
var queryBuilder = new sqlquery();
|
|
425
|
+
var query = queryBuilder.createIndex(indexInfo);
|
|
426
|
+
this.context._execute(query);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if(this.context.isPostgres){
|
|
430
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
431
|
+
var queryBuilder = new postgresQuery();
|
|
432
|
+
var query = queryBuilder.createIndex(indexInfo);
|
|
433
|
+
this.context._execute(query);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
dropIndex(indexInfo){
|
|
439
|
+
if(indexInfo){
|
|
440
|
+
if(this.context.isSQLite){
|
|
441
|
+
var sqliteQuery = require("./migrationSQLiteQuery");
|
|
442
|
+
var queryBuilder = new sqliteQuery();
|
|
443
|
+
var query = queryBuilder.dropIndex(indexInfo);
|
|
444
|
+
this.context._execute(query);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if(this.context.isMySQL){
|
|
448
|
+
var sqlquery = require("./migrationMySQLQuery");
|
|
449
|
+
var queryBuilder = new sqlquery();
|
|
450
|
+
var query = queryBuilder.dropIndex(indexInfo);
|
|
451
|
+
this.context._execute(query);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if(this.context.isPostgres){
|
|
455
|
+
var postgresQuery = require("./migrationPostgresQuery");
|
|
456
|
+
var queryBuilder = new postgresQuery();
|
|
457
|
+
var query = queryBuilder.dropIndex(indexInfo);
|
|
458
|
+
this.context._execute(query);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
397
463
|
seed(tableName, rows){
|
|
398
464
|
if(!tableName || !rows){ return; }
|
|
399
465
|
const items = Array.isArray(rows) ? rows : [rows];
|
|
@@ -196,6 +196,76 @@ class queryMethods{
|
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Get first record ordered by primary key
|
|
201
|
+
*/
|
|
202
|
+
async first() {
|
|
203
|
+
// Find primary key
|
|
204
|
+
let primaryKey = null;
|
|
205
|
+
for (const fieldName in this.__entity) {
|
|
206
|
+
if (this.__entity[fieldName]?.primary === true) {
|
|
207
|
+
primaryKey = fieldName;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (primaryKey && !this.__queryObject.script.orderBy) {
|
|
213
|
+
// Use proper orderBy syntax with lambda expression
|
|
214
|
+
const orderByExpr = `e => e.${primaryKey}`;
|
|
215
|
+
this.orderBy(orderByExpr);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.__queryObject.script.take = 1;
|
|
219
|
+
return await this.single();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get last record ordered by primary key descending
|
|
224
|
+
*/
|
|
225
|
+
async last() {
|
|
226
|
+
let primaryKey = null;
|
|
227
|
+
for (const fieldName in this.__entity) {
|
|
228
|
+
if (this.__entity[fieldName]?.primary === true) {
|
|
229
|
+
primaryKey = fieldName;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (primaryKey && !this.__queryObject.script.orderBy) {
|
|
235
|
+
// Use proper orderByDescending syntax with lambda expression
|
|
236
|
+
const orderByExpr = `e => e.${primaryKey}`;
|
|
237
|
+
this.orderByDescending(orderByExpr);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.__queryObject.script.take = 1;
|
|
241
|
+
return await this.single();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Check if any records match the query
|
|
246
|
+
*/
|
|
247
|
+
async exists() {
|
|
248
|
+
this.__queryObject.script.take = 1;
|
|
249
|
+
const result = await this.single();
|
|
250
|
+
return result !== null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Extract single column values as array
|
|
255
|
+
*/
|
|
256
|
+
async pluck(fieldName) {
|
|
257
|
+
if (!fieldName || typeof fieldName !== 'string') {
|
|
258
|
+
throw new Error('pluck() requires a field name string');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.__entity[fieldName]) {
|
|
262
|
+
throw new Error(`Field '${fieldName}' does not exist on ${this.__entity.__name}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const entities = await this.toList();
|
|
266
|
+
return entities.map(entity => entity[fieldName]);
|
|
267
|
+
}
|
|
268
|
+
|
|
199
269
|
/**
|
|
200
270
|
* Transform .includes() syntax to .any() syntax
|
|
201
271
|
* Converts: $$.includes(entity.field) => entity.field.any($$)
|
|
@@ -282,9 +352,10 @@ class queryMethods{
|
|
|
282
352
|
|
|
283
353
|
// Add array parameters and get comma-separated placeholders
|
|
284
354
|
const placeholders = this.__queryObject.parameters.addParams(itemArray, dbType);
|
|
285
|
-
// Replace $$
|
|
355
|
+
// Replace ONLY FIRST $$ occurrence (not all with /g flag)
|
|
356
|
+
// This ensures each parameter gets replaced in order
|
|
286
357
|
if(str.includes('$$')){
|
|
287
|
-
str = str.replace(
|
|
358
|
+
str = str.replace(/\$\$/, placeholders); // ✅ No 'g' flag - replace first only
|
|
288
359
|
} else {
|
|
289
360
|
// Replace single $ but not $N (postgres placeholders)
|
|
290
361
|
str = str.replace(/\$(?!\d)/, placeholders);
|
|
@@ -303,9 +374,10 @@ class queryMethods{
|
|
|
303
374
|
|
|
304
375
|
// Add parameter and replace placeholder
|
|
305
376
|
const placeholder = this.__queryObject.parameters.addParam(item, dbType);
|
|
306
|
-
// Replace $$
|
|
377
|
+
// Replace ONLY FIRST $$ occurrence (not all with /g flag)
|
|
378
|
+
// This ensures each parameter gets replaced in order
|
|
307
379
|
if(str.includes('$$')){
|
|
308
|
-
str = str.replace(
|
|
380
|
+
str = str.replace(/\$\$/, placeholder); // ✅ No 'g' flag - replace first only
|
|
309
381
|
} else {
|
|
310
382
|
// Replace single $ but not $N (postgres placeholders)
|
|
311
383
|
str = str.replace(/\$(?!\d)/, placeholder);
|
|
@@ -358,6 +430,7 @@ class queryMethods{
|
|
|
358
430
|
const cached = this.__context._queryCache.get(cacheKey);
|
|
359
431
|
if (cached) {
|
|
360
432
|
this.__reset();
|
|
433
|
+
// Cached entities already have methods - return directly
|
|
361
434
|
return cached;
|
|
362
435
|
}
|
|
363
436
|
}
|
|
@@ -407,6 +480,7 @@ class queryMethods{
|
|
|
407
480
|
const cached = this.__context._queryCache.get(cacheKey);
|
|
408
481
|
if (cached) {
|
|
409
482
|
this.__reset();
|
|
483
|
+
// Cached entities already have methods - return array directly
|
|
410
484
|
return cached;
|
|
411
485
|
}
|
|
412
486
|
}
|
|
@@ -465,6 +539,66 @@ class queryMethods{
|
|
|
465
539
|
enumerable: true,
|
|
466
540
|
configurable: true,
|
|
467
541
|
set: function(value) {
|
|
542
|
+
// Run validators before setting value
|
|
543
|
+
if (fieldDef && fieldDef.validators && Array.isArray(fieldDef.validators)) {
|
|
544
|
+
for (const validator of fieldDef.validators) {
|
|
545
|
+
let isValid = true;
|
|
546
|
+
let errorMsg = validator.message;
|
|
547
|
+
|
|
548
|
+
switch (validator.type) {
|
|
549
|
+
case 'required':
|
|
550
|
+
isValid = value !== null && value !== undefined && value !== '';
|
|
551
|
+
break;
|
|
552
|
+
|
|
553
|
+
case 'email':
|
|
554
|
+
if (value) {
|
|
555
|
+
isValid = validator.pattern.test(value);
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
|
|
559
|
+
case 'minLength':
|
|
560
|
+
if (value && typeof value === 'string') {
|
|
561
|
+
isValid = value.length >= validator.length;
|
|
562
|
+
}
|
|
563
|
+
break;
|
|
564
|
+
|
|
565
|
+
case 'maxLength':
|
|
566
|
+
if (value && typeof value === 'string') {
|
|
567
|
+
isValid = value.length <= validator.length;
|
|
568
|
+
}
|
|
569
|
+
break;
|
|
570
|
+
|
|
571
|
+
case 'pattern':
|
|
572
|
+
if (value) {
|
|
573
|
+
isValid = validator.pattern.test(value);
|
|
574
|
+
}
|
|
575
|
+
break;
|
|
576
|
+
|
|
577
|
+
case 'min':
|
|
578
|
+
if (value !== null && value !== undefined) {
|
|
579
|
+
isValid = Number(value) >= validator.min;
|
|
580
|
+
}
|
|
581
|
+
break;
|
|
582
|
+
|
|
583
|
+
case 'max':
|
|
584
|
+
if (value !== null && value !== undefined) {
|
|
585
|
+
isValid = Number(value) <= validator.max;
|
|
586
|
+
}
|
|
587
|
+
break;
|
|
588
|
+
|
|
589
|
+
case 'custom':
|
|
590
|
+
if (typeof validator.validator === 'function') {
|
|
591
|
+
isValid = validator.validator(value);
|
|
592
|
+
}
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (!isValid) {
|
|
597
|
+
throw new Error(`Validation failed: ${errorMsg}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
468
602
|
this.__proto__["_" + fname] = value;
|
|
469
603
|
if(!this.__dirtyFields.includes(fname)){
|
|
470
604
|
this.__dirtyFields.push(fname);
|
|
@@ -498,8 +632,200 @@ class queryMethods{
|
|
|
498
632
|
return await this.__context.saveChanges();
|
|
499
633
|
};
|
|
500
634
|
|
|
635
|
+
// Convert entity to plain JavaScript object
|
|
636
|
+
newEntity.toObject = function(options = {}) {
|
|
637
|
+
const includeRelationships = options.includeRelationships !== false;
|
|
638
|
+
const depth = options.depth || 1;
|
|
639
|
+
const visited = options._visited || new WeakSet();
|
|
640
|
+
|
|
641
|
+
// Prevent circular reference infinite loops
|
|
642
|
+
if (visited.has(this)) {
|
|
643
|
+
return { __circular: true, __entityName: this.__name, id: this[this.__primaryKey] };
|
|
644
|
+
}
|
|
645
|
+
visited.add(this);
|
|
646
|
+
|
|
647
|
+
const plain = {};
|
|
648
|
+
|
|
649
|
+
// Iterate through entity definition
|
|
650
|
+
for (const fieldName in this.__entity) {
|
|
651
|
+
if (fieldName.startsWith('__')) continue;
|
|
652
|
+
|
|
653
|
+
const fieldDef = this.__entity[fieldName];
|
|
654
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
655
|
+
fieldDef?.type === 'hasOne' ||
|
|
656
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
657
|
+
|
|
658
|
+
// Skip relationships in this pass
|
|
659
|
+
if (!isRelationship) {
|
|
660
|
+
try {
|
|
661
|
+
plain[fieldName] = this[fieldName];
|
|
662
|
+
} catch (e) {
|
|
663
|
+
// Skip fields that throw errors when accessed
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Handle relationships recursively with depth limit and cycle detection
|
|
669
|
+
if (includeRelationships && depth > 0) {
|
|
670
|
+
for (const fieldName in this.__entity) {
|
|
671
|
+
const fieldDef = this.__entity[fieldName];
|
|
672
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
673
|
+
fieldDef?.type === 'hasOne' ||
|
|
674
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
675
|
+
|
|
676
|
+
if (isRelationship) {
|
|
677
|
+
try {
|
|
678
|
+
const value = this[fieldName];
|
|
679
|
+
|
|
680
|
+
if (Array.isArray(value)) {
|
|
681
|
+
plain[fieldName] = value.map(item => {
|
|
682
|
+
if (item?.toObject && typeof item.toObject === 'function') {
|
|
683
|
+
return item.toObject({
|
|
684
|
+
depth: depth - 1,
|
|
685
|
+
_visited: visited
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return item;
|
|
689
|
+
});
|
|
690
|
+
} else if (value?.toObject && typeof value.toObject === 'function') {
|
|
691
|
+
plain[fieldName] = value.toObject({
|
|
692
|
+
depth: depth - 1,
|
|
693
|
+
_visited: visited
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
} catch (e) {
|
|
697
|
+
// Skip relationships that throw errors when accessed
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return plain;
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// JSON.stringify compatibility
|
|
707
|
+
newEntity.toJSON = function() {
|
|
708
|
+
return this.toObject({ includeRelationships: false });
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
// Delete entity from database
|
|
712
|
+
newEntity.delete = async function() {
|
|
713
|
+
if (!this.__context) {
|
|
714
|
+
throw new Error('Cannot delete: entity is not attached to a context');
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Mark entity for deletion
|
|
718
|
+
this.__state = 'delete';
|
|
719
|
+
|
|
720
|
+
// Ensure entity is tracked
|
|
721
|
+
if (!this.__context.__trackedEntitiesMap.has(this.__ID)) {
|
|
722
|
+
this.__context.__track(this);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Execute delete via saveChanges
|
|
726
|
+
return await this.__context.saveChanges();
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// Reload entity from database
|
|
730
|
+
newEntity.reload = async function() {
|
|
731
|
+
if (!this.__context) {
|
|
732
|
+
throw new Error('Cannot reload: entity is not attached to a context');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Get primary key
|
|
736
|
+
let primaryKey = null;
|
|
737
|
+
for (const fieldName in this.__entity) {
|
|
738
|
+
if (this.__entity[fieldName]?.primary === true) {
|
|
739
|
+
primaryKey = fieldName;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const primaryKeyValue = this[primaryKey];
|
|
745
|
+
|
|
746
|
+
if (!primaryKeyValue) {
|
|
747
|
+
throw new Error('Cannot reload: entity has no primary key value');
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Fetch fresh from database
|
|
751
|
+
const EntityClass = this.__context[this.__name];
|
|
752
|
+
const fresh = await EntityClass.findById(primaryKeyValue);
|
|
753
|
+
if (!fresh) {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Cannot reload: ${this.__name} with ${primaryKey}=${primaryKeyValue} not found`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Copy all field values from fresh entity to this entity
|
|
760
|
+
for (const fieldName in this.__entity) {
|
|
761
|
+
if (fieldName.startsWith('__')) continue;
|
|
762
|
+
|
|
763
|
+
const fieldDef = this.__entity[fieldName];
|
|
764
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
765
|
+
fieldDef?.type === 'hasOne' ||
|
|
766
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
767
|
+
|
|
768
|
+
// Only reload scalar fields
|
|
769
|
+
if (!isRelationship) {
|
|
770
|
+
this.__proto__["_" + fieldName] = fresh.__proto__["_" + fieldName];
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Reset dirty fields and state
|
|
775
|
+
this.__dirtyFields = [];
|
|
776
|
+
this.__state = 'track';
|
|
777
|
+
|
|
778
|
+
return this;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
// Clone entity for duplication
|
|
782
|
+
newEntity.clone = function() {
|
|
783
|
+
if (!this.__context) {
|
|
784
|
+
throw new Error('Cannot clone: entity is not attached to a context');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const EntityClass = this.__context[this.__name];
|
|
788
|
+
const cloned = EntityClass.new();
|
|
789
|
+
|
|
790
|
+
// Get primary key (to skip it)
|
|
791
|
+
let primaryKey = null;
|
|
792
|
+
for (const fieldName in this.__entity) {
|
|
793
|
+
if (this.__entity[fieldName]?.primary === true) {
|
|
794
|
+
primaryKey = fieldName;
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Copy all non-primary key fields
|
|
800
|
+
for (const fieldName in this.__entity) {
|
|
801
|
+
if (fieldName.startsWith('__')) continue;
|
|
802
|
+
if (fieldName === primaryKey) continue;
|
|
803
|
+
|
|
804
|
+
const fieldDef = this.__entity[fieldName];
|
|
805
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
806
|
+
fieldDef?.type === 'hasOne' ||
|
|
807
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
808
|
+
|
|
809
|
+
if (!isRelationship) {
|
|
810
|
+
cloned[fieldName] = this[fieldName];
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return cloned;
|
|
815
|
+
};
|
|
816
|
+
|
|
501
817
|
// Track the entity
|
|
502
818
|
this.__context.__track(newEntity);
|
|
819
|
+
|
|
820
|
+
// Copy lifecycle hooks from entity definition to entity instance
|
|
821
|
+
for (const fieldName in this.__entity) {
|
|
822
|
+
const fieldDef = this.__entity[fieldName];
|
|
823
|
+
if (fieldDef && fieldDef.lifecycle === true && fieldDef.method) {
|
|
824
|
+
// Bind the lifecycle hook method directly to this entity instance
|
|
825
|
+
newEntity[fieldName] = fieldDef.method.bind(newEntity);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
503
829
|
return newEntity;
|
|
504
830
|
}
|
|
505
831
|
|
package/SQLLiteEngine.js
CHANGED
|
@@ -583,6 +583,10 @@ class SQLLiteEngine {
|
|
|
583
583
|
var $that = this;
|
|
584
584
|
for (var ent in entity) {
|
|
585
585
|
if(!ent.startsWith("_")){
|
|
586
|
+
// Skip lifecycle hooks - they are not database columns
|
|
587
|
+
if(entity[ent].lifecycle === true){
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
586
590
|
if(!entity[ent].foreignKey){
|
|
587
591
|
if(entity[ent].relationshipTable){
|
|
588
592
|
if($that.chechUnsupportedWords(entity[ent].relationshipTable)){
|
package/Tools.js
CHANGED
|
@@ -84,7 +84,7 @@ class Tools{
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
static clearAllProto(proto){
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
var newproto = {}
|
|
89
89
|
if(proto.__proto__ ){
|
|
90
90
|
// Include non-enumerable own properties so we don't lose values defined via getters
|
|
@@ -93,6 +93,15 @@ class Tools{
|
|
|
93
93
|
if(!key.startsWith("_") && !key.startsWith("__")){
|
|
94
94
|
try{
|
|
95
95
|
const value = proto[key];
|
|
96
|
+
// Skip lifecycle hooks by checking entity definition
|
|
97
|
+
if(proto.__entity && proto.__entity[key] && proto.__entity[key].lifecycle === true){
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Skip functions EXCEPT if they're defined via getters (typeof returns value, not function)
|
|
101
|
+
// Only skip if it's actually a function value (methods like save, delete, toObject)
|
|
102
|
+
if(typeof value === "function"){
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
96
105
|
if(typeof value === "object" && value !== null){
|
|
97
106
|
// Recursively clone nested objects without altering the source
|
|
98
107
|
newproto[key] = this.clearAllProto(value);
|
|
@@ -180,10 +189,14 @@ class Tools{
|
|
|
180
189
|
// converts any object into SQL parameter select string
|
|
181
190
|
|
|
182
191
|
static convertEntityToSelectParameterString(obj){
|
|
183
|
-
// todo: loop throgh object and append string with comma to
|
|
192
|
+
// todo: loop throgh object and append string with comma to
|
|
184
193
|
var mainString = "";
|
|
185
194
|
const entries = Object.keys(obj);
|
|
186
195
|
for (const key of entries) {
|
|
196
|
+
// Skip lifecycle hooks - they are not database columns
|
|
197
|
+
if(obj[key].lifecycle === true){
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
187
200
|
if(obj[key].type !== 'hasManyThrough' && obj[key].type !== "hasMany" && obj[key].type !== "hasOne"){
|
|
188
201
|
if(obj[key].name){
|
|
189
202
|
mainString = mainString === "" ? `${obj.__name}.${obj[key].name}` : `${mainString}, ${obj.__name}.${obj[key].name}`;
|