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
|
@@ -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
|
@@ -111,10 +111,18 @@ class EntityModel {
|
|
|
111
111
|
|
|
112
112
|
unique(){
|
|
113
113
|
this.obj.unique = true; // yes
|
|
114
|
-
return this;
|
|
114
|
+
return this;
|
|
115
115
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
index(indexName){
|
|
119
|
+
if(!this.obj.indexes){
|
|
120
|
+
this.obj.indexes = [];
|
|
121
|
+
}
|
|
122
|
+
this.obj.indexes.push(indexName || true);
|
|
123
|
+
return this;
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
// this means that it can be an empty field
|
|
119
127
|
nullable(){
|
|
120
128
|
this.obj.nullable = true; // yes
|
|
@@ -217,5 +225,141 @@ class EntityModel {
|
|
|
217
225
|
this.obj.nullable = false;
|
|
218
226
|
return this
|
|
219
227
|
}
|
|
228
|
+
|
|
229
|
+
// ===== Validation Methods =====
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Validate that field value is required (not null, undefined, or empty string)
|
|
233
|
+
* @param {string} message - Custom error message
|
|
234
|
+
*/
|
|
235
|
+
required(message) {
|
|
236
|
+
if (!this.obj.validators) {
|
|
237
|
+
this.obj.validators = [];
|
|
238
|
+
}
|
|
239
|
+
this.obj.validators.push({
|
|
240
|
+
type: 'required',
|
|
241
|
+
message: message || `${this.obj.name} is required`
|
|
242
|
+
});
|
|
243
|
+
this.obj.nullable = false;
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Validate email format
|
|
249
|
+
* @param {string} message - Custom error message
|
|
250
|
+
*/
|
|
251
|
+
email(message) {
|
|
252
|
+
if (!this.obj.validators) {
|
|
253
|
+
this.obj.validators = [];
|
|
254
|
+
}
|
|
255
|
+
this.obj.validators.push({
|
|
256
|
+
type: 'email',
|
|
257
|
+
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
258
|
+
message: message || `${this.obj.name} must be a valid email address`
|
|
259
|
+
});
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validate minimum string length
|
|
265
|
+
* @param {number} length - Minimum length
|
|
266
|
+
* @param {string} message - Custom error message
|
|
267
|
+
*/
|
|
268
|
+
minLength(length, message) {
|
|
269
|
+
if (!this.obj.validators) {
|
|
270
|
+
this.obj.validators = [];
|
|
271
|
+
}
|
|
272
|
+
this.obj.validators.push({
|
|
273
|
+
type: 'minLength',
|
|
274
|
+
length: length,
|
|
275
|
+
message: message || `${this.obj.name} must be at least ${length} characters`
|
|
276
|
+
});
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Validate maximum string length
|
|
282
|
+
* @param {number} length - Maximum length
|
|
283
|
+
* @param {string} message - Custom error message
|
|
284
|
+
*/
|
|
285
|
+
maxLength(length, message) {
|
|
286
|
+
if (!this.obj.validators) {
|
|
287
|
+
this.obj.validators = [];
|
|
288
|
+
}
|
|
289
|
+
this.obj.validators.push({
|
|
290
|
+
type: 'maxLength',
|
|
291
|
+
length: length,
|
|
292
|
+
message: message || `${this.obj.name} must be at most ${length} characters`
|
|
293
|
+
});
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validate against regex pattern
|
|
299
|
+
* @param {RegExp} pattern - Regular expression to match
|
|
300
|
+
* @param {string} message - Custom error message
|
|
301
|
+
*/
|
|
302
|
+
pattern(regex, message) {
|
|
303
|
+
if (!this.obj.validators) {
|
|
304
|
+
this.obj.validators = [];
|
|
305
|
+
}
|
|
306
|
+
this.obj.validators.push({
|
|
307
|
+
type: 'pattern',
|
|
308
|
+
pattern: regex,
|
|
309
|
+
message: message || `${this.obj.name} format is invalid`
|
|
310
|
+
});
|
|
311
|
+
return this;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Validate minimum numeric value
|
|
316
|
+
* @param {number} min - Minimum value
|
|
317
|
+
* @param {string} message - Custom error message
|
|
318
|
+
*/
|
|
319
|
+
min(minValue, message) {
|
|
320
|
+
if (!this.obj.validators) {
|
|
321
|
+
this.obj.validators = [];
|
|
322
|
+
}
|
|
323
|
+
this.obj.validators.push({
|
|
324
|
+
type: 'min',
|
|
325
|
+
min: minValue,
|
|
326
|
+
message: message || `${this.obj.name} must be at least ${minValue}`
|
|
327
|
+
});
|
|
328
|
+
return this;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Validate maximum numeric value
|
|
333
|
+
* @param {number} max - Maximum value
|
|
334
|
+
* @param {string} message - Custom error message
|
|
335
|
+
*/
|
|
336
|
+
max(maxValue, message) {
|
|
337
|
+
if (!this.obj.validators) {
|
|
338
|
+
this.obj.validators = [];
|
|
339
|
+
}
|
|
340
|
+
this.obj.validators.push({
|
|
341
|
+
type: 'max',
|
|
342
|
+
max: maxValue,
|
|
343
|
+
message: message || `${this.obj.name} must be at most ${maxValue}`
|
|
344
|
+
});
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Custom validation function
|
|
350
|
+
* @param {Function} validatorFn - Function that returns true if valid, false if invalid
|
|
351
|
+
* @param {string} message - Custom error message
|
|
352
|
+
*/
|
|
353
|
+
custom(validatorFn, message) {
|
|
354
|
+
if (!this.obj.validators) {
|
|
355
|
+
this.obj.validators = [];
|
|
356
|
+
}
|
|
357
|
+
this.obj.validators.push({
|
|
358
|
+
type: 'custom',
|
|
359
|
+
validator: validatorFn,
|
|
360
|
+
message: message || `${this.obj.name} is invalid`
|
|
361
|
+
});
|
|
362
|
+
return this;
|
|
363
|
+
}
|
|
220
364
|
}
|
|
221
365
|
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
|
|
|
@@ -232,6 +232,20 @@ class migrationMySQLQuery {
|
|
|
232
232
|
return `ALTER TABLE \`${table.tableName}\` RENAME COLUMN \`${table.name}\` TO \`${table.newName}\``
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
createIndex(indexInfo){
|
|
236
|
+
const indexName = indexInfo.indexName === true
|
|
237
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
238
|
+
: indexInfo.indexName;
|
|
239
|
+
return `CREATE INDEX \`${indexName}\` ON \`${indexInfo.tableName}\`(\`${indexInfo.columnName}\`)`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dropIndex(indexInfo){
|
|
243
|
+
const indexName = indexInfo.indexName === true
|
|
244
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
245
|
+
: indexInfo.indexName;
|
|
246
|
+
return `DROP INDEX \`${indexName}\` ON \`${indexInfo.tableName}\``;
|
|
247
|
+
}
|
|
248
|
+
|
|
235
249
|
/**
|
|
236
250
|
* SEED DATA METHODS
|
|
237
251
|
* Support for inserting seed data during migrations
|
|
@@ -219,6 +219,20 @@ class migrationPostgresQuery {
|
|
|
219
219
|
return `ALTER TABLE "${table.tableName}" RENAME COLUMN "${table.name}" TO "${table.newName}"`;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
createIndex(indexInfo){
|
|
223
|
+
const indexName = indexInfo.indexName === true
|
|
224
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
225
|
+
: indexInfo.indexName;
|
|
226
|
+
return `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${indexInfo.tableName}"("${indexInfo.columnName}")`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
dropIndex(indexInfo){
|
|
230
|
+
const indexName = indexInfo.indexName === true
|
|
231
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
232
|
+
: indexInfo.indexName;
|
|
233
|
+
return `DROP INDEX IF EXISTS "${indexName}"`;
|
|
234
|
+
}
|
|
235
|
+
|
|
222
236
|
/**
|
|
223
237
|
* SEED DATA METHODS
|
|
224
238
|
* Support for inserting seed data during migrations
|
|
@@ -187,6 +187,20 @@ class migrationSQLiteQuery {
|
|
|
187
187
|
return `ALTER TABLE ${table.tableName} RENAME COLUMN ${table.name} TO ${table.newName}`
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
createIndex(indexInfo){
|
|
191
|
+
const indexName = indexInfo.indexName === true
|
|
192
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
193
|
+
: indexInfo.indexName;
|
|
194
|
+
return `CREATE INDEX IF NOT EXISTS ${indexName} ON ${indexInfo.tableName}(${indexInfo.columnName})`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
dropIndex(indexInfo){
|
|
198
|
+
const indexName = indexInfo.indexName === true
|
|
199
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
200
|
+
: indexInfo.indexName;
|
|
201
|
+
return `DROP INDEX IF EXISTS ${indexName}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
/**
|
|
191
205
|
* SEED DATA METHODS
|
|
192
206
|
* Support for inserting seed data during migrations
|
|
@@ -82,6 +82,44 @@ module.exports = ${this.name};
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
createIndex(type, indexInfo){
|
|
86
|
+
const indexName = indexInfo.indexName === true
|
|
87
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
88
|
+
: indexInfo.indexName;
|
|
89
|
+
|
|
90
|
+
const indexInfoStr = JSON.stringify({
|
|
91
|
+
tableName: indexInfo.tableName,
|
|
92
|
+
columnName: indexInfo.columnName,
|
|
93
|
+
indexName: indexInfo.indexName
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if(type === "up"){
|
|
97
|
+
this.#up += os.EOL + ` this.createIndex(${indexInfoStr});`
|
|
98
|
+
}
|
|
99
|
+
else{
|
|
100
|
+
this.#down += os.EOL + ` this.dropIndex(${indexInfoStr});`
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
dropIndex(type, indexInfo){
|
|
105
|
+
const indexName = indexInfo.indexName === true
|
|
106
|
+
? `idx_${indexInfo.tableName.toLowerCase()}_${indexInfo.columnName.toLowerCase()}`
|
|
107
|
+
: indexInfo.indexName;
|
|
108
|
+
|
|
109
|
+
const indexInfoStr = JSON.stringify({
|
|
110
|
+
tableName: indexInfo.tableName,
|
|
111
|
+
columnName: indexInfo.columnName,
|
|
112
|
+
indexName: indexInfo.indexName
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if(type === "up"){
|
|
116
|
+
this.#up += os.EOL + ` this.dropIndex(${indexInfoStr});`
|
|
117
|
+
}
|
|
118
|
+
else{
|
|
119
|
+
this.#down += os.EOL + ` this.createIndex(${indexInfoStr});`
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
85
123
|
}
|
|
86
124
|
|
|
87
125
|
module.exports = MigrationTemplate;
|