masterrecord 0.3.56 → 0.3.58
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 +36 -0
- package/package.json +1 -1
- package/test/promise-detection-test.js +290 -0
package/insertManager.js
CHANGED
|
@@ -342,6 +342,42 @@ class InsertManager {
|
|
|
342
342
|
continue;
|
|
343
343
|
}
|
|
344
344
|
|
|
345
|
+
// Detect non-primitive values (Promise, Array, plain Object) on scalar columns
|
|
346
|
+
const isRelationship = currentEntity.type === RELATIONSHIP_TYPES.BELONGS_TO ||
|
|
347
|
+
currentEntity.type === RELATIONSHIP_TYPES.HAS_MANY ||
|
|
348
|
+
currentEntity.type === RELATIONSHIP_TYPES.HAS_MANY_THROUGH ||
|
|
349
|
+
currentEntity.type === RELATIONSHIP_TYPES.HAS_ONE ||
|
|
350
|
+
currentEntity.relationshipType === RELATIONSHIP_TYPES.BELONGS_TO;
|
|
351
|
+
|
|
352
|
+
if (!isRelationship) {
|
|
353
|
+
const val = currentRealModel[entity];
|
|
354
|
+
if (val != null && typeof val === 'object') {
|
|
355
|
+
// Always reject Promises — a set() transform cannot meaningfully handle them
|
|
356
|
+
if (typeof val.then === 'function') {
|
|
357
|
+
const entityName = entityModel.__name || 'unknown';
|
|
358
|
+
this._errorModel.isValid = false;
|
|
359
|
+
this._errorModel.errors.push(
|
|
360
|
+
`Property '${entity}' on entity '${entityName}' contains a Promise. Did you forget to await an async call?`
|
|
361
|
+
);
|
|
362
|
+
} else if (!currentEntity.set) {
|
|
363
|
+
// Only flag Array/Object when there is no custom set() transform,
|
|
364
|
+
// since the setter may serialize them to a scalar (e.g. JSON.stringify)
|
|
365
|
+
const entityName = entityModel.__name || 'unknown';
|
|
366
|
+
if (Array.isArray(val)) {
|
|
367
|
+
this._errorModel.isValid = false;
|
|
368
|
+
this._errorModel.errors.push(
|
|
369
|
+
`Property '${entity}' on entity '${entityName}' contains an Array, expected a scalar value`
|
|
370
|
+
);
|
|
371
|
+
} else if (!(val instanceof Date)) {
|
|
372
|
+
this._errorModel.isValid = false;
|
|
373
|
+
this._errorModel.errors.push(
|
|
374
|
+
`Property '${entity}' on entity '${entityName}' contains an Object, expected a scalar value`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
345
381
|
// check if there is a default value
|
|
346
382
|
if (currentEntity.default) {
|
|
347
383
|
if (currentRealModel[entity] === undefined || currentRealModel[entity] === null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.58",
|
|
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": {
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Promise / non-primitive detection in InsertManager.validateEntity()
|
|
3
|
+
*
|
|
4
|
+
* Verifies that assigning a Promise, Array, or plain Object to a scalar
|
|
5
|
+
* entity column produces a clear validation error instead of being silently
|
|
6
|
+
* dropped from the INSERT statement.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Alias 'masterrecord' to local root so insertManager's require('masterrecord/...') resolves
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const Module = require('module');
|
|
12
|
+
const __MASTERRECORD_ROOT__ = path.join(__dirname, '..');
|
|
13
|
+
const __ORIGINAL_REQUIRE__ = Module.prototype.require;
|
|
14
|
+
Module.prototype.require = function(request) {
|
|
15
|
+
if (request === 'masterrecord' || request.startsWith('masterrecord/')) {
|
|
16
|
+
const resolved = request === 'masterrecord'
|
|
17
|
+
? __MASTERRECORD_ROOT__
|
|
18
|
+
: path.join(__MASTERRECORD_ROOT__, request.slice('masterrecord/'.length));
|
|
19
|
+
return __ORIGINAL_REQUIRE__.call(this, resolved);
|
|
20
|
+
}
|
|
21
|
+
return __ORIGINAL_REQUIRE__.call(this, request);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const InsertManager = require('../insertManager');
|
|
25
|
+
|
|
26
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
27
|
+
console.log("║ Promise / Non-Primitive Detection Test ║");
|
|
28
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
29
|
+
|
|
30
|
+
let passed = 0;
|
|
31
|
+
let failed = 0;
|
|
32
|
+
|
|
33
|
+
function assert(condition, label) {
|
|
34
|
+
if (condition) {
|
|
35
|
+
console.log(` ✓ ${label}`);
|
|
36
|
+
passed++;
|
|
37
|
+
} else {
|
|
38
|
+
console.log(` ✗ ${label}`);
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Minimal stubs — we only need validateEntity(), no SQL engine
|
|
44
|
+
function makeErrorModel() {
|
|
45
|
+
return { isValid: true, errors: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function makeManager() {
|
|
49
|
+
const err = makeErrorModel();
|
|
50
|
+
const mgr = new InsertManager(null, err, []);
|
|
51
|
+
return { mgr, err };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Entity definition for a simple "User" table
|
|
55
|
+
const entityModel = {
|
|
56
|
+
__name: 'User',
|
|
57
|
+
id: { type: 'integer', primary: true, auto: true, nullable: true },
|
|
58
|
+
user_name: { type: 'string', nullable: false },
|
|
59
|
+
age: { type: 'integer', nullable: true },
|
|
60
|
+
role: { type: 'string', nullable: true },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// -----------------------------------------------------------
|
|
64
|
+
// Test 1: Promise assigned to a string column
|
|
65
|
+
// -----------------------------------------------------------
|
|
66
|
+
console.log("Test 1: Promise on string column");
|
|
67
|
+
console.log("──────────────────────────────────────────────────");
|
|
68
|
+
{
|
|
69
|
+
const { mgr, err } = makeManager();
|
|
70
|
+
const realModel = { user_name: Promise.resolve('alice'), age: 25, __entity: entityModel };
|
|
71
|
+
const cleanModel = { user_name: Promise.resolve('alice'), age: 25, __entity: entityModel };
|
|
72
|
+
|
|
73
|
+
mgr.validateEntity(cleanModel, realModel, entityModel);
|
|
74
|
+
|
|
75
|
+
assert(!err.isValid, 'Validation fails');
|
|
76
|
+
assert(err.errors.length === 1, 'Exactly one error');
|
|
77
|
+
assert(err.errors[0].includes('Promise'), 'Error mentions Promise');
|
|
78
|
+
assert(err.errors[0].includes('user_name'), 'Error mentions field name');
|
|
79
|
+
assert(err.errors[0].includes('await'), 'Error suggests await');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// -----------------------------------------------------------
|
|
83
|
+
// Test 2: Array assigned to a string column
|
|
84
|
+
// -----------------------------------------------------------
|
|
85
|
+
console.log("\nTest 2: Array on string column");
|
|
86
|
+
console.log("──────────────────────────────────────────────────");
|
|
87
|
+
{
|
|
88
|
+
const { mgr, err } = makeManager();
|
|
89
|
+
const realModel = { user_name: 'alice', role: ['admin', 'user'], __entity: entityModel };
|
|
90
|
+
const cleanModel = { ...realModel };
|
|
91
|
+
|
|
92
|
+
mgr.validateEntity(cleanModel, realModel, entityModel);
|
|
93
|
+
|
|
94
|
+
assert(!err.isValid, 'Validation fails');
|
|
95
|
+
assert(err.errors[0].includes('Array'), 'Error mentions Array');
|
|
96
|
+
assert(err.errors[0].includes('role'), 'Error mentions field name');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// -----------------------------------------------------------
|
|
100
|
+
// Test 3: Plain object assigned to an integer column
|
|
101
|
+
// -----------------------------------------------------------
|
|
102
|
+
console.log("\nTest 3: Plain object on integer column");
|
|
103
|
+
console.log("──────────────────────────────────────────────────");
|
|
104
|
+
{
|
|
105
|
+
const { mgr, err } = makeManager();
|
|
106
|
+
const realModel = { user_name: 'alice', age: { value: 25 }, __entity: entityModel };
|
|
107
|
+
const cleanModel = { ...realModel };
|
|
108
|
+
|
|
109
|
+
mgr.validateEntity(cleanModel, realModel, entityModel);
|
|
110
|
+
|
|
111
|
+
assert(!err.isValid, 'Validation fails');
|
|
112
|
+
assert(err.errors[0].includes('Object'), 'Error mentions Object');
|
|
113
|
+
assert(err.errors[0].includes('age'), 'Error mentions field name');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// -----------------------------------------------------------
|
|
117
|
+
// Test 4: Normal scalar values pass validation
|
|
118
|
+
// -----------------------------------------------------------
|
|
119
|
+
console.log("\nTest 4: Normal scalar values pass validation");
|
|
120
|
+
console.log("──────────────────────────────────────────────────");
|
|
121
|
+
{
|
|
122
|
+
const { mgr, err } = makeManager();
|
|
123
|
+
const realModel = { user_name: 'alice', age: 25, role: 'admin', __entity: entityModel };
|
|
124
|
+
const cleanModel = { ...realModel };
|
|
125
|
+
|
|
126
|
+
mgr.validateEntity(cleanModel, realModel, entityModel);
|
|
127
|
+
|
|
128
|
+
assert(err.isValid, 'Validation passes');
|
|
129
|
+
assert(err.errors.length === 0, 'No errors');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// -----------------------------------------------------------
|
|
133
|
+
// Test 5: Date objects are allowed (not flagged)
|
|
134
|
+
// -----------------------------------------------------------
|
|
135
|
+
console.log("\nTest 5: Date objects are allowed");
|
|
136
|
+
console.log("──────────────────────────────────────────────────");
|
|
137
|
+
{
|
|
138
|
+
const entityWithDate = {
|
|
139
|
+
__name: 'Event',
|
|
140
|
+
id: { type: 'integer', primary: true, auto: true, nullable: true },
|
|
141
|
+
event_date: { type: 'string', nullable: true },
|
|
142
|
+
};
|
|
143
|
+
const { mgr, err } = makeManager();
|
|
144
|
+
const realModel = { event_date: new Date(), __entity: entityWithDate };
|
|
145
|
+
const cleanModel = { ...realModel };
|
|
146
|
+
|
|
147
|
+
mgr.validateEntity(cleanModel, realModel, entityWithDate);
|
|
148
|
+
|
|
149
|
+
assert(err.isValid, 'Date objects pass validation');
|
|
150
|
+
assert(err.errors.length === 0, 'No errors for Date');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// -----------------------------------------------------------
|
|
154
|
+
// Test 6: Relationship columns are NOT flagged for objects
|
|
155
|
+
// -----------------------------------------------------------
|
|
156
|
+
console.log("\nTest 6: Relationship columns allow objects");
|
|
157
|
+
console.log("──────────────────────────────────────────────────");
|
|
158
|
+
{
|
|
159
|
+
const entityWithRel = {
|
|
160
|
+
__name: 'Post',
|
|
161
|
+
id: { type: 'integer', primary: true, auto: true, nullable: true },
|
|
162
|
+
title: { type: 'string', nullable: false },
|
|
163
|
+
author: { type: 'belongsTo', relationshipType: 'belongsTo', foreignKey: 'author_id', name: 'author' },
|
|
164
|
+
tags: { type: 'hasMany', name: 'tags' },
|
|
165
|
+
meta: { type: 'hasOne', name: 'meta' },
|
|
166
|
+
cats: { type: 'hasManyThrough', name: 'cats' },
|
|
167
|
+
};
|
|
168
|
+
const { mgr, err } = makeManager();
|
|
169
|
+
const realModel = {
|
|
170
|
+
title: 'Hello',
|
|
171
|
+
author: { id: 1, name: 'Alice' },
|
|
172
|
+
tags: [{ id: 1 }],
|
|
173
|
+
meta: { key: 'val' },
|
|
174
|
+
cats: [{ id: 2 }],
|
|
175
|
+
__entity: entityWithRel,
|
|
176
|
+
};
|
|
177
|
+
const cleanModel = { ...realModel };
|
|
178
|
+
|
|
179
|
+
mgr.validateEntity(cleanModel, realModel, entityWithRel);
|
|
180
|
+
|
|
181
|
+
assert(err.isValid, 'Relationship objects pass validation');
|
|
182
|
+
assert(err.errors.length === 0, 'No errors for relationship objects');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// -----------------------------------------------------------
|
|
186
|
+
// Test 7: null and undefined values are NOT flagged
|
|
187
|
+
// -----------------------------------------------------------
|
|
188
|
+
console.log("\nTest 7: null/undefined values are not flagged");
|
|
189
|
+
console.log("──────────────────────────────────────────────────");
|
|
190
|
+
{
|
|
191
|
+
const { mgr, err } = makeManager();
|
|
192
|
+
const realModel = { user_name: 'alice', age: null, role: undefined, __entity: entityModel };
|
|
193
|
+
const cleanModel = { ...realModel };
|
|
194
|
+
|
|
195
|
+
mgr.validateEntity(cleanModel, realModel, entityModel);
|
|
196
|
+
|
|
197
|
+
assert(err.isValid, 'null/undefined pass validation');
|
|
198
|
+
assert(err.errors.length === 0, 'No errors for null/undefined');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// -----------------------------------------------------------
|
|
202
|
+
// Test 8: Multiple bad fields produce multiple errors
|
|
203
|
+
// -----------------------------------------------------------
|
|
204
|
+
console.log("\nTest 8: Multiple bad fields produce multiple errors");
|
|
205
|
+
console.log("──────────────────────────────────────────────────");
|
|
206
|
+
{
|
|
207
|
+
const { mgr, err } = makeManager();
|
|
208
|
+
const realModel = {
|
|
209
|
+
user_name: Promise.resolve('alice'),
|
|
210
|
+
age: [1, 2, 3],
|
|
211
|
+
role: { x: 1 },
|
|
212
|
+
__entity: entityModel,
|
|
213
|
+
};
|
|
214
|
+
const cleanModel = { ...realModel };
|
|
215
|
+
|
|
216
|
+
mgr.validateEntity(cleanModel, realModel, entityModel);
|
|
217
|
+
|
|
218
|
+
assert(!err.isValid, 'Validation fails');
|
|
219
|
+
assert(err.errors.length === 3, 'Three errors (Promise + Array + Object)');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// -----------------------------------------------------------
|
|
223
|
+
// Test 9: Array allowed when field has a custom set() transform
|
|
224
|
+
// -----------------------------------------------------------
|
|
225
|
+
console.log("\nTest 9: Array allowed with custom set() transform");
|
|
226
|
+
console.log("──────────────────────────────────────────────────");
|
|
227
|
+
{
|
|
228
|
+
const entityWithSetter = {
|
|
229
|
+
__name: 'BatchJob',
|
|
230
|
+
id: { type: 'integer', primary: true, auto: true, nullable: true },
|
|
231
|
+
name: { type: 'string', nullable: false },
|
|
232
|
+
error_log: { type: 'string', nullable: true, set: function(value) {
|
|
233
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
234
|
+
return value;
|
|
235
|
+
}},
|
|
236
|
+
config_snapshot: { type: 'string', nullable: true, set: function(value) {
|
|
237
|
+
if (typeof value === 'object' && value !== null) return JSON.stringify(value);
|
|
238
|
+
return value;
|
|
239
|
+
}},
|
|
240
|
+
};
|
|
241
|
+
const { mgr, err } = makeManager();
|
|
242
|
+
const realModel = {
|
|
243
|
+
name: 'job1',
|
|
244
|
+
error_log: ['err1', 'err2'],
|
|
245
|
+
config_snapshot: { retries: 3 },
|
|
246
|
+
__entity: entityWithSetter,
|
|
247
|
+
};
|
|
248
|
+
const cleanModel = { ...realModel };
|
|
249
|
+
|
|
250
|
+
mgr.validateEntity(cleanModel, realModel, entityWithSetter);
|
|
251
|
+
|
|
252
|
+
assert(err.isValid, 'Array with set() passes validation');
|
|
253
|
+
assert(err.errors.length === 0, 'No errors when set() is defined');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// -----------------------------------------------------------
|
|
257
|
+
// Test 10: Promise still rejected even when field has set()
|
|
258
|
+
// -----------------------------------------------------------
|
|
259
|
+
console.log("\nTest 10: Promise rejected even with custom set()");
|
|
260
|
+
console.log("──────────────────────────────────────────────────");
|
|
261
|
+
{
|
|
262
|
+
const entityWithSetter = {
|
|
263
|
+
__name: 'BatchJob',
|
|
264
|
+
id: { type: 'integer', primary: true, auto: true, nullable: true },
|
|
265
|
+
error_log: { type: 'string', nullable: true, set: function(value) {
|
|
266
|
+
if (Array.isArray(value)) return JSON.stringify(value);
|
|
267
|
+
return value;
|
|
268
|
+
}},
|
|
269
|
+
};
|
|
270
|
+
const { mgr, err } = makeManager();
|
|
271
|
+
const realModel = {
|
|
272
|
+
error_log: Promise.resolve(['err1']),
|
|
273
|
+
__entity: entityWithSetter,
|
|
274
|
+
};
|
|
275
|
+
const cleanModel = { ...realModel };
|
|
276
|
+
|
|
277
|
+
mgr.validateEntity(cleanModel, realModel, entityWithSetter);
|
|
278
|
+
|
|
279
|
+
assert(!err.isValid, 'Promise still rejected with set()');
|
|
280
|
+
assert(err.errors[0].includes('Promise'), 'Error mentions Promise');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Summary
|
|
284
|
+
console.log("\n" + "=".repeat(64));
|
|
285
|
+
console.log(`Test Results: ${passed} passed, ${failed} failed`);
|
|
286
|
+
console.log("=".repeat(64));
|
|
287
|
+
|
|
288
|
+
if (failed > 0) {
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|