masterrecord 0.3.9 → 0.3.11

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/insertManager.js CHANGED
@@ -1,250 +1,408 @@
1
+ 'use strict';
1
2
 
2
- // version 0.0.15
3
- var tools = require('./Tools');
4
- var queryScript = require('masterrecord/QueryLanguage/queryScript');
3
+ // version 1.0.0 - FAANG-level refactor
4
+ const tools = require('./Tools');
5
+ const queryScript = require('masterrecord/QueryLanguage/queryScript');
5
6
 
6
- class InsertManager {
7
+ // Constants
8
+ const TIMESTAMP_FIELDS = {
9
+ CREATED_AT: 'created_at',
10
+ UPDATED_AT: 'updated_at'
11
+ };
12
+
13
+ const RELATIONSHIP_TYPES = {
14
+ HAS_MANY: 'hasMany',
15
+ HAS_MANY_THROUGH: 'hasManyThrough',
16
+ BELONGS_TO: 'belongsTo',
17
+ HAS_ONE: 'hasOne'
18
+ };
19
+
20
+ const MIN_OBJECT_KEYS = 0;
21
+
22
+ // Custom Error Classes
23
+ class InsertManagerError extends Error {
24
+ constructor(message, context = {}) {
25
+ super(message);
26
+ this.name = 'InsertManagerError';
27
+ this.context = context;
28
+ Error.captureStackTrace(this, this.constructor);
29
+ }
30
+ }
7
31
 
8
- constructor(sqlEngine, errorModel, allEntities ){
32
+ class RelationshipError extends InsertManagerError {
33
+ constructor(message, relationshipName, context = {}) {
34
+ super(message, { ...context, relationshipName });
35
+ this.name = 'RelationshipError';
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Insert Manager - Handles entity insertion with relationship processing
41
+ *
42
+ * Manages INSERT operations for tracked entities including:
43
+ * - Relationship hydration (hasMany, hasManyThrough, belongsTo, hasOne)
44
+ * - Validation of required fields
45
+ * - Error aggregation and reporting
46
+ * - Automatic timestamp management
47
+ *
48
+ * @class InsertManager
49
+ * @example
50
+ * const manager = new InsertManager(sqlEngine, errorModel, allEntities);
51
+ * manager.init(trackedEntity);
52
+ */
53
+ class InsertManager {
54
+ /**
55
+ * Creates an insert manager instance
56
+ *
57
+ * @param {object} sqlEngine - Database engine instance (SQLite/MySQL/Postgres)
58
+ * @param {object} errorModel - Validation error collector
59
+ * @param {Array<object>} allEntities - All registered entity definitions
60
+ */
61
+ constructor(sqlEngine, errorModel, allEntities) {
9
62
  this._SQLEngine = sqlEngine;
10
63
  this._errorModel = errorModel;
11
64
  this._allEntities = allEntities;
12
65
  this.__queryObject = new queryScript();
13
66
  }
14
67
 
15
- init(currentModel){
68
+ /**
69
+ * Initialize insert operation for a tracked entity
70
+ *
71
+ * @param {object} currentModel - Tracked entity to insert
72
+ * @throws {InsertManagerError} If validation fails
73
+ */
74
+ init(currentModel) {
16
75
  this.runQueries(currentModel);
17
76
  }
18
77
 
19
- runQueries(currentModel){
20
- var $that = this;
78
+ /**
79
+ * Execute insert queries with relationship processing
80
+ *
81
+ * @param {object} currentModel - Tracked entity to insert
82
+ * @throws {InsertManagerError} If validation fails or relationships are invalid
83
+ */
84
+ runQueries(currentModel) {
21
85
  // Reset validation state for this operation to avoid stale errors
22
- if(this._errorModel){
86
+ if (this._errorModel) {
23
87
  this._errorModel.isValid = true;
24
88
  this._errorModel.errors = [];
25
89
  }
26
- var cleanCurrentModel = tools.clearAllProto(currentModel);
90
+
91
+ const cleanCurrentModel = tools.clearAllProto(currentModel);
27
92
  this.validateEntity(cleanCurrentModel, currentModel, currentModel.__entity);
28
- if(this._errorModel.isValid){
29
-
30
- var modelEntity = currentModel.__entity;
31
- // TODO: if you try to add belongs to you must have a tag added first. if you dont throw error
32
- currentModel = this.belongsToInsert(currentModel, modelEntity);
33
- var SQL = this._SQLEngine.insert(cleanCurrentModel);
34
- var primaryKey = tools.getPrimaryKeyObject(currentModel.__entity);
35
- // use returned insert id directly; avoid redundant post-insert SELECT
36
- if(currentModel.__entity[primaryKey].auto === true){
37
- currentModel[primaryKey] = SQL.id;
93
+
94
+ if (this._errorModel.isValid) {
95
+ const modelEntity = currentModel.__entity;
96
+ // TODO: if you try to add belongs to you must have a tag added first. if you dont throw error
97
+ currentModel = this.belongsToInsert(currentModel, modelEntity);
98
+ const SQL = this._SQLEngine.insert(cleanCurrentModel);
99
+ const primaryKey = tools.getPrimaryKeyObject(currentModel.__entity);
100
+
101
+ // use returned insert id directly; avoid redundant post-insert SELECT
102
+ if (currentModel.__entity[primaryKey].auto === true) {
103
+ currentModel[primaryKey] = SQL.id;
104
+ }
105
+
106
+ const proto = Object.getPrototypeOf(currentModel);
107
+ const props = Object.getOwnPropertyNames(proto);
108
+ const cleanPropList = tools.returnEntityList(props, modelEntity);
109
+ const modelKeys = Object.keys(currentModel);
110
+ const mergedArray = [...new Set(modelKeys.concat(cleanPropList))];
111
+
112
+ // loop through model properties
113
+ for (const property of mergedArray) {
114
+ const propertyModel = currentModel[property];
115
+ const entityProperty = modelEntity[property] ? modelEntity[property] : {};
116
+
117
+ if (entityProperty.type === RELATIONSHIP_TYPES.HAS_ONE) {
118
+ this._processHasOneRelationship(propertyModel, entityProperty, property, currentModel, SQL);
38
119
  }
39
120
 
40
- const proto = Object.getPrototypeOf(currentModel);
41
- const props = Object.getOwnPropertyNames(proto);
42
- const cleanPropList = tools.returnEntityList(props, modelEntity);
43
- const modelKeys = Object.keys(currentModel);
44
- const mergedArray = [...new Set(modelKeys.concat(cleanPropList))];
45
- // loop through model properties
46
- for (const property of mergedArray) {
47
- var propertyModel = currentModel[property];
48
- var entityProperty = modelEntity[property] ? modelEntity[property] : {};
49
- if(entityProperty.type === "hasOne"){
50
- // only insert child if user provided a concrete object with data
51
- if(propertyModel && typeof(propertyModel) === "object" ){
52
- // check if model has its own entity
53
- if(modelEntity){
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
- }
76
- }
77
- else{
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`
79
- }
80
- }
81
- }
82
-
83
- if(entityProperty.type === "hasMany"){
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)){
89
- const propertyKeys = Object.keys(propertyModel);
90
- for (const propertykey of propertyKeys) {
91
- if(propertyModel[propertykey]){
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
- }
121
+ if (entityProperty.type === RELATIONSHIP_TYPES.HAS_MANY) {
122
+ this._processArrayRelationship(propertyModel, entityProperty, property, currentModel, SQL, RELATIONSHIP_TYPES.HAS_MANY);
123
+ }
124
+
125
+ if (entityProperty.type === RELATIONSHIP_TYPES.HAS_MANY_THROUGH) {
126
+ this._processArrayRelationship(propertyModel, entityProperty, property, currentModel, SQL, RELATIONSHIP_TYPES.HAS_MANY_THROUGH);
127
+ }
128
+ }
129
+ } else {
130
+ const messages = this._errorModel.errors;
131
+ const combinedError = messages.join('; and ');
132
+ throw new InsertManagerError(combinedError, {
133
+ errors: messages,
134
+ entity: currentModel.__entity ? currentModel.__entity.__name : 'unknown'
135
+ });
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Process hasOne relationship
141
+ *
142
+ * @private
143
+ * @param {*} propertyModel - Property value
144
+ * @param {object} entityProperty - Entity property definition
145
+ * @param {string} property - Property name
146
+ * @param {object} currentModel - Current model being inserted
147
+ * @param {object} SQL - SQL result from parent insert
148
+ * @throws {RelationshipError} If relationship entity is not found
149
+ */
150
+ _processHasOneRelationship(propertyModel, entityProperty, property, currentModel, SQL) {
151
+ // only insert child if user provided a concrete object with data
152
+ if (propertyModel && typeof propertyModel === 'object') {
153
+ // check if model has its own entity
154
+ const modelEntity = currentModel.__entity;
155
+ if (!modelEntity) {
156
+ throw new RelationshipError(
157
+ `Relationship "${entityProperty.name}" could not be found. Please check if object has correct spelling or if it has been added to the context class`,
158
+ entityProperty.name,
159
+ { property }
160
+ );
161
+ }
162
+
163
+ // check if property has a value because we dont want this to run on every insert if nothing was added
164
+ // ensure it has some own props; otherwise skip
165
+ const hasOwn = Object.keys(propertyModel).length > MIN_OBJECT_KEYS;
166
+ if (!hasOwn) {
167
+ return;
168
+ }
169
+
170
+ propertyModel.__entity = tools.getEntity(property, this._allEntities);
171
+ propertyModel[currentModel.__entity.__name] = SQL.id;
172
+ this.runQueries(propertyModel);
173
+
174
+ // Hydrate child back as a tracked instance so subsequent property sets are tracked
175
+ try {
176
+ const childPk = tools.getPrimaryKeyObject(propertyModel.__entity);
177
+ const childId = propertyModel[childPk];
178
+
179
+ if (childId !== undefined) {
180
+ // Validate identifier is safe for SQL queries
181
+ if (!this._isValidIdentifier(childPk)) {
182
+ throw new InsertManagerError(
183
+ `Invalid primary key identifier: ${childPk}`,
184
+ { childPk, property }
185
+ );
115
186
  }
116
187
 
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;
136
- propertyModel[propertykey][currentModel.__entity.__name] = SQL.id;
137
- $that.runQueries(propertyModel[propertykey]);
138
- }
139
- }
140
- }
141
- else{
142
- throw `Relationship "${entityProperty.name}" must be an array`;
188
+ const ctxSetName = tools.capitalizeFirstLetter(property);
189
+ if (currentModel.__context && currentModel.__context[ctxSetName]) {
190
+ const trackedChild = currentModel.__context[ctxSetName]
191
+ .where(`r => r.${childPk} == $$`, childId)
192
+ .single();
193
+ if (trackedChild) {
194
+ currentModel[property] = trackedChild;
143
195
  }
144
196
  }
145
-
146
197
  }
198
+ } catch (error) {
199
+ // Log but don't throw - hydration is optional
200
+ console.warn('[InsertManager] Entity hydration failed:', {
201
+ property,
202
+ error: error.message,
203
+ childId: propertyModel[tools.getPrimaryKeyObject(propertyModel.__entity)]
204
+ });
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Process array-type relationships (hasMany, hasManyThrough)
211
+ *
212
+ * @private
213
+ * @param {*} propertyModel - Property value (should be array-like)
214
+ * @param {object} entityProperty - Entity property definition
215
+ * @param {string} property - Property name
216
+ * @param {object} currentModel - Current model being inserted
217
+ * @param {object} SQL - SQL result from parent insert
218
+ * @param {string} relationshipType - 'hasMany' or 'hasManyThrough'
219
+ * @throws {RelationshipError} If validation fails or entity not resolved
220
+ */
221
+ _processArrayRelationship(propertyModel, entityProperty, property, currentModel, SQL, relationshipType) {
222
+ // skip when not provided; only enforce array type if user supplied a value
223
+ if (propertyModel === undefined || propertyModel === null) {
224
+ return;
147
225
  }
148
- else{
149
- var messages = this._errorModel.errors;
150
- const combinedError = messages.join('; and ');
151
- throw combinedError;
152
226
 
227
+ if (!tools.checkIfArrayLike(propertyModel)) {
228
+ throw new RelationshipError(
229
+ `Relationship "${entityProperty.name}" must be an array`,
230
+ entityProperty.name,
231
+ { property, relationshipType, receivedType: typeof propertyModel }
232
+ );
153
233
  }
234
+
235
+ const propertyKeys = Object.keys(propertyModel);
236
+ for (const propertykey of propertyKeys) {
237
+ if (!propertyModel[propertykey]) {
238
+ continue;
239
+ }
240
+
241
+ const targetName = entityProperty.foreignTable || property;
242
+ const resolved = this._resolveEntityWithFallback(property, targetName);
243
+
244
+ if (!resolved) {
245
+ throw new RelationshipError(
246
+ `Relationship entity for '${property}' could not be resolved. Expected '${targetName}'.`,
247
+ property,
248
+ {
249
+ targetName,
250
+ relationshipType,
251
+ availableEntities: this._allEntities.map(e => e.__name)
252
+ }
253
+ );
254
+ }
255
+
256
+ // Coerce primitive into object with primary key if user passed an id
257
+ if (typeof propertyModel[propertykey] !== 'object' || propertyModel[propertykey] === null) {
258
+ const childPrimaryKey = tools.getPrimaryKeyObject(resolved);
259
+ const primitiveValue = propertyModel[propertykey];
260
+ propertyModel[propertykey] = {};
261
+ propertyModel[propertykey][childPrimaryKey] = primitiveValue;
262
+ }
263
+
264
+ propertyModel[propertykey].__entity = resolved;
265
+ propertyModel[propertykey][currentModel.__entity.__name] = SQL.id;
266
+ this.runQueries(propertyModel[propertykey]);
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Resolve entity with multiple fallback strategies
272
+ *
273
+ * @private
274
+ * @param {string} property - Property name
275
+ * @param {string} targetName - Target entity name
276
+ * @returns {object|null} Resolved entity or null
277
+ */
278
+ _resolveEntityWithFallback(property, targetName) {
279
+ // Try: exact match → capitalized → property name
280
+ return tools.getEntity(targetName, this._allEntities)
281
+ || tools.getEntity(tools.capitalize(targetName), this._allEntities)
282
+ || tools.getEntity(property, this._allEntities);
154
283
  }
155
284
 
156
-
157
-
158
- // will insert belongs to row first and return the id so that next call can be make correctly
159
- belongsToInsert(currentModel, modelEntity){
160
- var $that = this;
161
- for(var entity in modelEntity) {
162
- if(modelEntity[entity].relationshipType === "belongsTo"){
163
- var foreignKey = modelEntity[entity].foreignKey === undefined ? modelEntity[entity].name : modelEntity[entity].foreignKey;
164
- var newPropertyModel = currentModel[foreignKey];
165
- // check if model is a an object. If so insert the child first then the parent.
166
- if(typeof newPropertyModel === 'object'){
167
- newPropertyModel.__entity = tools.getEntity(entity, $that._allEntities);
168
- var propertyCleanCurrentModel = tools.clearAllProto(newPropertyModel);
285
+ /**
286
+ * Validate identifier is safe for SQL queries
287
+ *
288
+ * @private
289
+ * @param {string} identifier - Identifier to validate
290
+ * @returns {boolean} True if safe
291
+ */
292
+ _isValidIdentifier(identifier) {
293
+ // Allow only alphanumeric and underscore, must start with letter/underscore
294
+ return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier);
295
+ }
296
+
297
+ /**
298
+ * Insert belongsTo relationships first and return updated model
299
+ * Will insert belongs to row first and return the id so that next call can be made correctly
300
+ *
301
+ * @param {object} currentModel - Current model being inserted
302
+ * @param {object} modelEntity - Entity definition
303
+ * @returns {object} Updated model with foreign keys populated
304
+ */
305
+ belongsToInsert(currentModel, modelEntity) {
306
+ for (const entity of Object.keys(modelEntity)) {
307
+ if (modelEntity[entity].relationshipType === RELATIONSHIP_TYPES.BELONGS_TO) {
308
+ const foreignKey = modelEntity[entity].foreignKey === undefined
309
+ ? modelEntity[entity].name
310
+ : modelEntity[entity].foreignKey;
311
+ const newPropertyModel = currentModel[foreignKey];
312
+
313
+ // check if model is an object. If so insert the child first then the parent.
314
+ if (typeof newPropertyModel === 'object' && newPropertyModel !== null) {
315
+ newPropertyModel.__entity = tools.getEntity(entity, this._allEntities);
316
+ const propertyCleanCurrentModel = tools.clearAllProto(newPropertyModel);
169
317
  this.validateEntity(propertyCleanCurrentModel, newPropertyModel, newPropertyModel.__entity);
170
- var propertySQL = this._SQLEngine.insert(newPropertyModel);
171
- currentModel[foreignKey] = propertySQL.id;
318
+ const propertySQL = this._SQLEngine.insert(newPropertyModel);
319
+ currentModel[foreignKey] = propertySQL.id;
172
320
  }
173
321
  }
174
322
  }
175
323
  // todo:
176
- // loop through all modelEntity and find all the belongs to
177
- // if belongs to is true then make sql call to insert
178
- // update the currentModel.
324
+ // loop through all modelEntity and find all the belongs to
325
+ // if belongs to is true then make sql call to insert
326
+ // update the currentModel.
179
327
  return currentModel;
180
328
  }
181
329
 
182
- // validate entity for nullable fields and if the entity has any values at all
183
- validateEntity(currentModel, currentRealModel, entityModel){
184
- for(var entity in entityModel) {
185
- var currentEntity = entityModel[entity];
186
- if (entityModel.hasOwnProperty(entity)) {
187
- // check if there is a default value
188
- if(currentEntity.default){
189
- if(currentRealModel[entity] === undefined || currentRealModel[entity] === null){
190
- // if its empty add the default value
191
- currentRealModel[entity] = currentEntity.default;
192
- }
330
+ /**
331
+ * Validate entity for nullable fields and if the entity has any values at all
332
+ *
333
+ * @param {object} currentModel - Clean model (no prototypes)
334
+ * @param {object} currentRealModel - Real tracked model
335
+ * @param {object} entityModel - Entity definition
336
+ */
337
+ validateEntity(currentModel, currentRealModel, entityModel) {
338
+ for (const entity of Object.keys(entityModel)) {
339
+ const currentEntity = entityModel[entity];
340
+
341
+ if (!entityModel.hasOwnProperty(entity)) {
342
+ continue;
343
+ }
344
+
345
+ // check if there is a default value
346
+ if (currentEntity.default) {
347
+ if (currentRealModel[entity] === undefined || currentRealModel[entity] === null) {
348
+ // if its empty add the default value
349
+ currentRealModel[entity] = currentEntity.default;
193
350
  }
194
-
195
- // SKIP belongs too ----- // call sets for correct data for DB
196
- if(currentEntity.type !== "belongsTo" && currentEntity.type !== "hasMany"){
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
- }
204
- // primary is always null in an insert so validation insert must be null
205
- if(currentEntity.nullable === false && !currentEntity.primary){
206
- // if it doesnt have a get method then call error
207
- if(currentEntity.set === undefined){
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){
227
- this._errorModel.isValid = false;
228
- var errorMessage = `Entity ${currentModel.__entity.__name} column ${entity} is a required Field`;
229
- this._errorModel.errors.push(errorMessage);
230
- //throw errorMessage;
351
+ }
352
+
353
+ // SKIP belongs too ----- // call sets for correct data for DB
354
+ if (currentEntity.type !== RELATIONSHIP_TYPES.BELONGS_TO && currentEntity.type !== RELATIONSHIP_TYPES.HAS_MANY) {
355
+ if (currentEntity.relationshipType !== RELATIONSHIP_TYPES.BELONGS_TO) {
356
+ // Auto-populate common timestamp fields if required and missing
357
+ if ((entity === TIMESTAMP_FIELDS.CREATED_AT || entity === TIMESTAMP_FIELDS.UPDATED_AT) &&
358
+ (currentRealModel[entity] === undefined || currentRealModel[entity] === null)) {
359
+ const nowVal = Date.now().toString();
360
+ currentRealModel[entity] = nowVal;
361
+ currentModel[entity] = nowVal;
362
+ }
363
+
364
+ // primary is always null in an insert so validation insert must be null
365
+ if (currentEntity.nullable === false && !currentEntity.primary) {
366
+ // if it doesnt have a get method then call error
367
+ if (currentEntity.set === undefined) {
368
+ const realVal = currentRealModel[entity];
369
+ const cleanVal = currentModel[entity];
370
+ let hasValue = (realVal !== undefined && realVal !== null) ||
371
+ (cleanVal !== undefined && cleanVal !== null);
372
+
373
+ // For strings, empty string should be considered invalid for notNullable
374
+ const candidate = (realVal !== undefined && realVal !== null) ? realVal : cleanVal;
375
+ if (typeof candidate === 'string' && candidate.trim() === '') {
376
+ hasValue = false;
377
+ }
378
+
379
+ // Fallback: check backing field on tracked model if both reads were undefined/null
380
+ if (!hasValue && currentRealModel && currentRealModel.__proto__) {
381
+ const backing = currentRealModel.__proto__['_' + entity];
382
+ hasValue = (backing !== undefined && backing !== null);
383
+ if (hasValue) {
384
+ // normalize into both models so downstream sees it
385
+ currentRealModel[entity] = backing;
386
+ currentModel[entity] = backing;
231
387
  }
232
388
  }
233
- else{
234
- var realData = currentEntity.set(currentModel[entity]);
235
- currentRealModel[entity] = realData;
236
- currentModel[entity] = realData;
389
+
390
+ if (!hasValue) {
391
+ this._errorModel.isValid = false;
392
+ const errorMessage = `Entity ${currentModel.__entity.__name} column ${entity} is a required Field`;
393
+ this._errorModel.errors.push(errorMessage);
394
+ //throw errorMessage;
237
395
  }
396
+ } else {
397
+ const realData = currentEntity.set(currentModel[entity]);
398
+ currentRealModel[entity] = realData;
399
+ currentModel[entity] = realData;
238
400
  }
239
401
  }
240
-
241
402
  }
242
403
  }
243
-
244
404
  }
245
405
  }
246
-
247
406
  }
248
407
 
249
-
250
408
  module.exports = InsertManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {