masterrecord 0.3.27 → 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.
@@ -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": []
@@ -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[methodNamesArray[i]](MDB);
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 = methodNamesArray[i];
29
- obj[methodNamesArray[i]] = MDB.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