masterrecord 0.3.8 → 0.3.10
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/.eslintrc.js +290 -0
- package/.prettierrc.js +109 -0
- package/CHANGES.md +170 -0
- package/Entity/entityTrackerModel.js +17 -3
- package/Migrations/cli.js +4 -2
- package/Migrations/migrations.js +13 -10
- package/Migrations/pathUtils.js +76 -0
- package/Migrations/pathUtils.test.js +53 -0
- package/QueryLanguage/queryMethods.js +15 -0
- package/context.js +1186 -398
- package/deleteManager.js +137 -40
- package/docs/ACTIVE_RECORD_PATTERN.md +477 -0
- package/docs/DETACHED_ENTITIES_GUIDE.md +445 -0
- package/insertManager.js +358 -200
- package/package.json +1 -1
- package/readme.md +217 -7
- package/test/attachDetached.test.js +303 -0
- /package/{QUERY_CACHING_GUIDE.md → docs/QUERY_CACHING_GUIDE.md} +0 -0
package/insertManager.js
CHANGED
|
@@ -1,250 +1,408 @@
|
|
|
1
|
+
'use strict';
|
|
1
2
|
|
|
2
|
-
// version
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
// version 1.0.0 - FAANG-level refactor
|
|
4
|
+
const tools = require('./Tools');
|
|
5
|
+
const queryScript = require('masterrecord/QueryLanguage/queryScript');
|
|
5
6
|
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
90
|
+
|
|
91
|
+
const cleanCurrentModel = tools.clearAllProto(currentModel);
|
|
27
92
|
this.validateEntity(cleanCurrentModel, currentModel, currentModel.__entity);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if(
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
currentModel
|
|
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.
|
|
3
|
+
"version": "0.3.10",
|
|
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": {
|