masterrecord 0.3.26 → 0.3.29
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 +136 -0
- package/Entity/entityModelBuilder.js +21 -3
- package/Entity/entityTrackerModel.js +251 -1
- 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 +1070 -102
- 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
|
@@ -38,7 +38,27 @@
|
|
|
38
38
|
"Bash(master=development masterrecord update-database qaContext)",
|
|
39
39
|
"Bash(chmod:*)",
|
|
40
40
|
"Bash(npm install:*)",
|
|
41
|
-
"Bash(sqlite3:*)"
|
|
41
|
+
"Bash(sqlite3:*)",
|
|
42
|
+
"Bash(for file in /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training/components/models/app/controllers/api/*.js)",
|
|
43
|
+
"Bash(do)",
|
|
44
|
+
"Bash(if grep -q \"this._currentUser = req.authService.currentUser\" \"$file\")",
|
|
45
|
+
"Bash(then)",
|
|
46
|
+
"Bash(fi)",
|
|
47
|
+
"Bash(echo:*)",
|
|
48
|
+
"Bash(/tmp/fix_controllers.sh)",
|
|
49
|
+
"Bash(wc -l echo \"\" echo \"Controllers now using async pattern:\" find /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training/components -name \"*Controller.js\" -path \"*/controllers/api/*\" -exec grep -l \"async _getCurrentUser\\(\\)\" {})",
|
|
50
|
+
"Bash(/tmp/fix_controllers.sh:*)",
|
|
51
|
+
"Bash(wc -l echo \"\" echo \"Total with async _getCurrentUser\\(\\):\" find /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training/components -name \"*Controller.js\" -path \"*/controllers/api/*\" -exec grep -l \"async _getCurrentUser\\(\\)\" {})",
|
|
52
|
+
"Bash(wc -l echo echo 'Controllers with async _getCurrentUser\\(\\):' find /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training/components -name *Controller.js -path */controllers/* -exec grep -l 'async _getCurrentUser\\(\\)' {})",
|
|
53
|
+
"Bash(sort -u __NEW_LINE_463d116b631aaffc__ echo 'Actual controller files \\(without .js extension\\):' find /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training/components/*/app/controllers/api -name *.js -type f)",
|
|
54
|
+
"Bash(xargs:*)",
|
|
55
|
+
"Bash(sort -u __NEW_LINE_463d116b631aaffc__ echo echo 'Files that exist but not referenced in routes \\(might be OK\\):' comm -13 /tmp/route_refs.txt /tmp/actual_files.txt)",
|
|
56
|
+
"Bash(head -10 __NEW_LINE_463d116b631aaffc__ echo \"\" echo \"Routes referencing non-existent files \\(PROBLEMS\\):\" comm -23 /tmp/route_refs.txt /tmp/actual_files.txt)",
|
|
57
|
+
"Bash(for file in /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training/components/qa/app/controllers/api/*.js)",
|
|
58
|
+
"Bash(wc:*)",
|
|
59
|
+
"Bash(npm link)",
|
|
60
|
+
"Bash(npm link:*)",
|
|
61
|
+
"Bash(1)"
|
|
42
62
|
],
|
|
43
63
|
"deny": [],
|
|
44
64
|
"ask": []
|
package/Entity/entityModel.js
CHANGED
|
@@ -217,5 +217,141 @@ class EntityModel {
|
|
|
217
217
|
this.obj.nullable = false;
|
|
218
218
|
return this
|
|
219
219
|
}
|
|
220
|
+
|
|
221
|
+
// ===== Validation Methods =====
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Validate that field value is required (not null, undefined, or empty string)
|
|
225
|
+
* @param {string} message - Custom error message
|
|
226
|
+
*/
|
|
227
|
+
required(message) {
|
|
228
|
+
if (!this.obj.validators) {
|
|
229
|
+
this.obj.validators = [];
|
|
230
|
+
}
|
|
231
|
+
this.obj.validators.push({
|
|
232
|
+
type: 'required',
|
|
233
|
+
message: message || `${this.obj.name} is required`
|
|
234
|
+
});
|
|
235
|
+
this.obj.nullable = false;
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Validate email format
|
|
241
|
+
* @param {string} message - Custom error message
|
|
242
|
+
*/
|
|
243
|
+
email(message) {
|
|
244
|
+
if (!this.obj.validators) {
|
|
245
|
+
this.obj.validators = [];
|
|
246
|
+
}
|
|
247
|
+
this.obj.validators.push({
|
|
248
|
+
type: 'email',
|
|
249
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
250
|
+
message: message || `${this.obj.name} must be a valid email address`
|
|
251
|
+
});
|
|
252
|
+
return this;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Validate minimum string length
|
|
257
|
+
* @param {number} length - Minimum length
|
|
258
|
+
* @param {string} message - Custom error message
|
|
259
|
+
*/
|
|
260
|
+
minLength(length, message) {
|
|
261
|
+
if (!this.obj.validators) {
|
|
262
|
+
this.obj.validators = [];
|
|
263
|
+
}
|
|
264
|
+
this.obj.validators.push({
|
|
265
|
+
type: 'minLength',
|
|
266
|
+
length: length,
|
|
267
|
+
message: message || `${this.obj.name} must be at least ${length} characters`
|
|
268
|
+
});
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Validate maximum string length
|
|
274
|
+
* @param {number} length - Maximum length
|
|
275
|
+
* @param {string} message - Custom error message
|
|
276
|
+
*/
|
|
277
|
+
maxLength(length, message) {
|
|
278
|
+
if (!this.obj.validators) {
|
|
279
|
+
this.obj.validators = [];
|
|
280
|
+
}
|
|
281
|
+
this.obj.validators.push({
|
|
282
|
+
type: 'maxLength',
|
|
283
|
+
length: length,
|
|
284
|
+
message: message || `${this.obj.name} must be at most ${length} characters`
|
|
285
|
+
});
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Validate against regex pattern
|
|
291
|
+
* @param {RegExp} pattern - Regular expression to match
|
|
292
|
+
* @param {string} message - Custom error message
|
|
293
|
+
*/
|
|
294
|
+
pattern(regex, message) {
|
|
295
|
+
if (!this.obj.validators) {
|
|
296
|
+
this.obj.validators = [];
|
|
297
|
+
}
|
|
298
|
+
this.obj.validators.push({
|
|
299
|
+
type: 'pattern',
|
|
300
|
+
pattern: regex,
|
|
301
|
+
message: message || `${this.obj.name} format is invalid`
|
|
302
|
+
});
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Validate minimum numeric value
|
|
308
|
+
* @param {number} min - Minimum value
|
|
309
|
+
* @param {string} message - Custom error message
|
|
310
|
+
*/
|
|
311
|
+
min(minValue, message) {
|
|
312
|
+
if (!this.obj.validators) {
|
|
313
|
+
this.obj.validators = [];
|
|
314
|
+
}
|
|
315
|
+
this.obj.validators.push({
|
|
316
|
+
type: 'min',
|
|
317
|
+
min: minValue,
|
|
318
|
+
message: message || `${this.obj.name} must be at least ${minValue}`
|
|
319
|
+
});
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Validate maximum numeric value
|
|
325
|
+
* @param {number} max - Maximum value
|
|
326
|
+
* @param {string} message - Custom error message
|
|
327
|
+
*/
|
|
328
|
+
max(maxValue, message) {
|
|
329
|
+
if (!this.obj.validators) {
|
|
330
|
+
this.obj.validators = [];
|
|
331
|
+
}
|
|
332
|
+
this.obj.validators.push({
|
|
333
|
+
type: 'max',
|
|
334
|
+
max: maxValue,
|
|
335
|
+
message: message || `${this.obj.name} must be at most ${maxValue}`
|
|
336
|
+
});
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Custom validation function
|
|
342
|
+
* @param {Function} validatorFn - Function that returns true if valid, false if invalid
|
|
343
|
+
* @param {string} message - Custom error message
|
|
344
|
+
*/
|
|
345
|
+
custom(validatorFn, message) {
|
|
346
|
+
if (!this.obj.validators) {
|
|
347
|
+
this.obj.validators = [];
|
|
348
|
+
}
|
|
349
|
+
this.obj.validators.push({
|
|
350
|
+
type: 'custom',
|
|
351
|
+
validator: validatorFn,
|
|
352
|
+
message: message || `${this.obj.name} is invalid`
|
|
353
|
+
});
|
|
354
|
+
return this;
|
|
355
|
+
}
|
|
220
356
|
}
|
|
221
357
|
module.exports = EntityModel;
|
|
@@ -17,16 +17,34 @@ class EntityModelBuilder {
|
|
|
17
17
|
if (constructorIndex > -1) {
|
|
18
18
|
methodNamesArray.splice(constructorIndex, 1);
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
// Define lifecycle hook method names that should not be treated as field definitions
|
|
22
|
+
const lifecycleHooks = ['beforeSave', 'afterSave', 'beforeDelete', 'afterDelete'];
|
|
23
|
+
|
|
20
24
|
// loop through all method names in the entity model
|
|
21
25
|
for (var i = 0; i < methodNamesArray.length; i++) {
|
|
26
|
+
const methodName = methodNamesArray[i];
|
|
27
|
+
|
|
28
|
+
// Skip lifecycle hooks - they should not be called during entity construction
|
|
29
|
+
if (lifecycleHooks.includes(methodName)) {
|
|
30
|
+
// Store lifecycle hooks with the actual method function so they can be copied to entity instances
|
|
31
|
+
obj[methodName] = {
|
|
32
|
+
virtual: true,
|
|
33
|
+
lifecycle: true,
|
|
34
|
+
name: methodName,
|
|
35
|
+
method: mod[methodName] // Store the method (not bound yet - will be bound to entity instance)
|
|
36
|
+
};
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
22
40
|
let MDB = new modelDB(model.name); // create a new instance of entity Model class
|
|
23
|
-
mod[
|
|
41
|
+
mod[methodName](MDB);
|
|
24
42
|
this.cleanNull(MDB.obj); // remove objects that are null or undefined
|
|
25
43
|
if(Object.keys(MDB.obj).length === 0){
|
|
26
44
|
MDB.obj.virtual = true;
|
|
27
45
|
}
|
|
28
|
-
MDB.obj.name =
|
|
29
|
-
obj[
|
|
46
|
+
MDB.obj.name = methodName;
|
|
47
|
+
obj[methodName] = MDB.obj;
|
|
30
48
|
}
|
|
31
49
|
return obj;
|
|
32
50
|
}
|
|
@@ -14,11 +14,12 @@ class EntityTrackerModel {
|
|
|
14
14
|
var modelClass = this.buildObject(); // build entity with models
|
|
15
15
|
modelClass.__proto__ = {};
|
|
16
16
|
const modelFields = Object.entries(dataModel); /// return array of objects
|
|
17
|
+
|
|
17
18
|
modelClass.__entity = currentEntity;
|
|
18
19
|
modelClass.__name = currentEntity.__name;
|
|
19
20
|
modelClass.__context = context;
|
|
20
21
|
this.buildRelationshipModels(modelClass, currentEntity, dataModel);
|
|
21
|
-
|
|
22
|
+
|
|
22
23
|
// loop through data model fields
|
|
23
24
|
for (const [modelField, modelFieldValue] of modelFields) {
|
|
24
25
|
|
|
@@ -44,6 +45,67 @@ class EntityTrackerModel {
|
|
|
44
45
|
|
|
45
46
|
Object.defineProperty(modelClass,modelField, {
|
|
46
47
|
set: function(value) {
|
|
48
|
+
// Run validators before setting value
|
|
49
|
+
const fieldDef = currentEntity[modelField];
|
|
50
|
+
if (fieldDef && fieldDef.validators && Array.isArray(fieldDef.validators)) {
|
|
51
|
+
for (const validator of fieldDef.validators) {
|
|
52
|
+
let isValid = true;
|
|
53
|
+
let errorMsg = validator.message;
|
|
54
|
+
|
|
55
|
+
switch (validator.type) {
|
|
56
|
+
case 'required':
|
|
57
|
+
isValid = value !== null && value !== undefined && value !== '';
|
|
58
|
+
break;
|
|
59
|
+
|
|
60
|
+
case 'email':
|
|
61
|
+
if (value) {
|
|
62
|
+
isValid = validator.pattern.test(value);
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case 'minLength':
|
|
67
|
+
if (value && typeof value === 'string') {
|
|
68
|
+
isValid = value.length >= validator.length;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
|
|
72
|
+
case 'maxLength':
|
|
73
|
+
if (value && typeof value === 'string') {
|
|
74
|
+
isValid = value.length <= validator.length;
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
|
|
78
|
+
case 'pattern':
|
|
79
|
+
if (value) {
|
|
80
|
+
isValid = validator.pattern.test(value);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 'min':
|
|
85
|
+
if (value !== null && value !== undefined) {
|
|
86
|
+
isValid = Number(value) >= validator.min;
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
|
|
90
|
+
case 'max':
|
|
91
|
+
if (value !== null && value !== undefined) {
|
|
92
|
+
isValid = Number(value) <= validator.max;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
|
|
96
|
+
case 'custom':
|
|
97
|
+
if (typeof validator.validator === 'function') {
|
|
98
|
+
isValid = validator.validator(value);
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isValid) {
|
|
104
|
+
throw new Error(`Validation failed: ${errorMsg}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
47
109
|
modelClass.__state = "modified";
|
|
48
110
|
modelClass.__dirtyFields.push(modelField);
|
|
49
111
|
// ensure this entity is tracked on any modification
|
|
@@ -93,6 +155,194 @@ class EntityTrackerModel {
|
|
|
93
155
|
return await this.__context.saveChanges();
|
|
94
156
|
};
|
|
95
157
|
|
|
158
|
+
// Convert entity to plain JavaScript object
|
|
159
|
+
modelClass.toObject = function(options = {}) {
|
|
160
|
+
const includeRelationships = options.includeRelationships !== false;
|
|
161
|
+
const depth = options.depth || 1;
|
|
162
|
+
const visited = options._visited || new WeakSet();
|
|
163
|
+
|
|
164
|
+
// Prevent circular reference infinite loops
|
|
165
|
+
if (visited.has(this)) {
|
|
166
|
+
return { __circular: true, __entityName: this.__name, id: this[this.__primaryKey] };
|
|
167
|
+
}
|
|
168
|
+
visited.add(this);
|
|
169
|
+
|
|
170
|
+
const plain = {};
|
|
171
|
+
|
|
172
|
+
// Method 1: Access internal _values property (v0.3.28+ architecture)
|
|
173
|
+
// This is the FASTEST method - direct access to plain data storage
|
|
174
|
+
if (this._values && typeof this._values === 'object') {
|
|
175
|
+
for (const key in this._values) {
|
|
176
|
+
if (this._values.hasOwnProperty(key)) {
|
|
177
|
+
plain[key] = this._values[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
// Method 2: Fallback - iterate through entity definition (for older versions)
|
|
182
|
+
for (const fieldName in this.__entity) {
|
|
183
|
+
if (fieldName.startsWith('__')) continue;
|
|
184
|
+
|
|
185
|
+
const fieldDef = this.__entity[fieldName];
|
|
186
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
187
|
+
fieldDef?.type === 'hasOne' ||
|
|
188
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
189
|
+
|
|
190
|
+
// Skip relationships in this pass
|
|
191
|
+
if (!isRelationship) {
|
|
192
|
+
try {
|
|
193
|
+
plain[fieldName] = this[fieldName];
|
|
194
|
+
} catch (e) {
|
|
195
|
+
// Skip fields that throw errors when accessed
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Handle relationships recursively with depth limit and cycle detection
|
|
202
|
+
if (includeRelationships && depth > 0) {
|
|
203
|
+
for (const fieldName in this.__entity) {
|
|
204
|
+
const fieldDef = this.__entity[fieldName];
|
|
205
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
206
|
+
fieldDef?.type === 'hasOne' ||
|
|
207
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
208
|
+
|
|
209
|
+
if (isRelationship) {
|
|
210
|
+
try {
|
|
211
|
+
const value = this[fieldName];
|
|
212
|
+
|
|
213
|
+
if (Array.isArray(value)) {
|
|
214
|
+
plain[fieldName] = value.map(item => {
|
|
215
|
+
if (item?.toObject && typeof item.toObject === 'function') {
|
|
216
|
+
return item.toObject({
|
|
217
|
+
depth: depth - 1,
|
|
218
|
+
_visited: visited
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return item;
|
|
222
|
+
});
|
|
223
|
+
} else if (value?.toObject && typeof value.toObject === 'function') {
|
|
224
|
+
plain[fieldName] = value.toObject({
|
|
225
|
+
depth: depth - 1,
|
|
226
|
+
_visited: visited
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// Skip relationships that throw errors when accessed
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return plain;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// JSON.stringify compatibility - prevents circular reference errors
|
|
240
|
+
modelClass.toJSON = function() {
|
|
241
|
+
return this.toObject({ includeRelationships: false });
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// Delete entity from database
|
|
245
|
+
modelClass.delete = async function() {
|
|
246
|
+
if (!this.__context) {
|
|
247
|
+
throw new Error('Cannot delete: entity is not attached to a context');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Mark entity for deletion
|
|
251
|
+
this.__state = 'delete';
|
|
252
|
+
|
|
253
|
+
// Ensure entity is tracked
|
|
254
|
+
if (!this.__context.__trackedEntitiesMap.has(this.__ID)) {
|
|
255
|
+
this.__context.__track(this);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Execute delete via saveChanges (handles cascade deletion)
|
|
259
|
+
return await this.__context.saveChanges();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// Reload entity from database
|
|
263
|
+
modelClass.reload = async function() {
|
|
264
|
+
if (!this.__context) {
|
|
265
|
+
throw new Error('Cannot reload: entity is not attached to a context');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Get primary key
|
|
269
|
+
const primaryKey = tools.getPrimaryKeyObject(this.__entity);
|
|
270
|
+
const primaryKeyValue = this[primaryKey];
|
|
271
|
+
|
|
272
|
+
if (!primaryKeyValue) {
|
|
273
|
+
throw new Error('Cannot reload: entity has no primary key value');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Fetch fresh from database
|
|
277
|
+
const EntityClass = this.__context[this.__name];
|
|
278
|
+
const fresh = await EntityClass.findById(primaryKeyValue);
|
|
279
|
+
if (!fresh) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Cannot reload: ${this.__name} with ${primaryKey}=${primaryKeyValue} not found`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Copy all field values from fresh entity to this entity
|
|
286
|
+
for (const fieldName in this.__entity) {
|
|
287
|
+
if (fieldName.startsWith('__')) continue;
|
|
288
|
+
|
|
289
|
+
const fieldDef = this.__entity[fieldName];
|
|
290
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
291
|
+
fieldDef?.type === 'hasOne' ||
|
|
292
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
293
|
+
|
|
294
|
+
// Only reload scalar fields
|
|
295
|
+
if (!isRelationship) {
|
|
296
|
+
this.__proto__["_" + fieldName] = fresh.__proto__["_" + fieldName];
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Reset dirty fields and state
|
|
301
|
+
this.__dirtyFields = [];
|
|
302
|
+
this.__state = 'track';
|
|
303
|
+
|
|
304
|
+
return this;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Clone entity for duplication
|
|
308
|
+
modelClass.clone = function() {
|
|
309
|
+
if (!this.__context) {
|
|
310
|
+
throw new Error('Cannot clone: entity is not attached to a context');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const EntityClass = this.__context[this.__name];
|
|
314
|
+
const cloned = EntityClass.new();
|
|
315
|
+
|
|
316
|
+
// Get primary key (to skip it)
|
|
317
|
+
const primaryKey = tools.getPrimaryKeyObject(this.__entity);
|
|
318
|
+
|
|
319
|
+
// Copy all non-primary key fields
|
|
320
|
+
for (const fieldName in this.__entity) {
|
|
321
|
+
if (fieldName.startsWith('__')) continue;
|
|
322
|
+
if (fieldName === primaryKey) continue;
|
|
323
|
+
|
|
324
|
+
const fieldDef = this.__entity[fieldName];
|
|
325
|
+
const isRelationship = fieldDef?.type === 'hasMany' ||
|
|
326
|
+
fieldDef?.type === 'hasOne' ||
|
|
327
|
+
fieldDef?.relationshipType === 'belongsTo';
|
|
328
|
+
|
|
329
|
+
if (!isRelationship) {
|
|
330
|
+
cloned[fieldName] = this[fieldName];
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return cloned;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Copy lifecycle hooks from entity definition to entity instance
|
|
338
|
+
for (const fieldName in currentEntity) {
|
|
339
|
+
const fieldDef = currentEntity[fieldName];
|
|
340
|
+
if (fieldDef && fieldDef.lifecycle === true && fieldDef.method) {
|
|
341
|
+
// Bind the lifecycle hook method directly to this entity instance
|
|
342
|
+
modelClass[fieldName] = fieldDef.method.bind(modelClass);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
96
346
|
return modelClass;
|
|
97
347
|
}
|
|
98
348
|
|