masterrecord 0.3.37 → 0.3.39
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 +2 -1
- package/FOREIGN_KEY_STRING_FIX.md +288 -0
- package/GLOBAL_REGISTRY_VERIFICATION.md +375 -0
- package/SQLLiteEngine.js +8 -0
- package/context.js +29 -4
- package/mySQLEngine.js +8 -0
- package/package.json +1 -1
- package/postgresEngine.js +8 -0
- package/readme.md +182 -0
- package/test/foreign-key-string-value-test.js +406 -0
- package/test/global-model-registry-test.js +538 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Foreign Key String Value Bug Fix
|
|
3
|
+
*
|
|
4
|
+
* Verifies that string values assigned to foreign key fields (defined via belongsTo)
|
|
5
|
+
* are properly auto-converted to integers and included in INSERT statements.
|
|
6
|
+
*
|
|
7
|
+
* Bug: MasterRecord was silently ignoring string values for foreign key fields,
|
|
8
|
+
* causing NOT NULL constraint failures.
|
|
9
|
+
*
|
|
10
|
+
* Fix: Added check in _buildSQLInsertObjectParameterized to look for values
|
|
11
|
+
* in both the navigation property name (e.g., 'User') AND the foreign key field name (e.g., 'user_id').
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
15
|
+
console.log("║ Foreign Key String Value Test - INSERT Bug Fix ║");
|
|
16
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
17
|
+
|
|
18
|
+
let passed = 0;
|
|
19
|
+
let failed = 0;
|
|
20
|
+
|
|
21
|
+
// Simulate the SQLite engine's INSERT builder
|
|
22
|
+
class SimulatedSQLiteEngine {
|
|
23
|
+
_buildSQLInsertObjectParameterized(fields, modelEntity) {
|
|
24
|
+
const columnNames = [];
|
|
25
|
+
const params = [];
|
|
26
|
+
|
|
27
|
+
for (const column in modelEntity) {
|
|
28
|
+
if (column.indexOf("__") === -1) {
|
|
29
|
+
let fieldColumn = fields[column];
|
|
30
|
+
|
|
31
|
+
// 🔥 FIX: For belongsTo relationships, also check the foreignKey field name
|
|
32
|
+
if ((fieldColumn === undefined || fieldColumn === null) &&
|
|
33
|
+
modelEntity[column].relationshipType === "belongsTo" &&
|
|
34
|
+
modelEntity[column].foreignKey) {
|
|
35
|
+
fieldColumn = fields[modelEntity[column].foreignKey];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if ((fieldColumn !== undefined && fieldColumn !== null) && typeof(fieldColumn) !== "object") {
|
|
39
|
+
// Auto-convert string to integer for integer fields
|
|
40
|
+
if (modelEntity[column].type === "integer" && typeof fieldColumn === "string") {
|
|
41
|
+
const parsed = parseInt(fieldColumn, 10);
|
|
42
|
+
if (isNaN(parsed)) {
|
|
43
|
+
throw new Error(`Cannot convert "${fieldColumn}" to integer`);
|
|
44
|
+
}
|
|
45
|
+
fieldColumn = parsed;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const relationship = modelEntity[column].relationshipType;
|
|
49
|
+
const actualColumn = relationship === "belongsTo" ? modelEntity[column].foreignKey : column;
|
|
50
|
+
|
|
51
|
+
columnNames.push(`[${actualColumn}]`);
|
|
52
|
+
params.push(fieldColumn);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (columnNames.length > 0) {
|
|
58
|
+
const placeholders = params.map(() => '?').join(', ');
|
|
59
|
+
return {
|
|
60
|
+
tableName: modelEntity.__name,
|
|
61
|
+
columns: columnNames.join(', '),
|
|
62
|
+
placeholders: placeholders,
|
|
63
|
+
params: params
|
|
64
|
+
};
|
|
65
|
+
} else {
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Test helper
|
|
72
|
+
function test(description, fn) {
|
|
73
|
+
try {
|
|
74
|
+
fn();
|
|
75
|
+
passed++;
|
|
76
|
+
console.log(`✓ ${description}`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
failed++;
|
|
79
|
+
console.log(`✗ ${description}`);
|
|
80
|
+
console.log(` Error: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// TEST 1: String value for foreign key field (the reported bug)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
test('Should include string foreign key value in INSERT', () => {
|
|
89
|
+
const engine = new SimulatedSQLiteEngine();
|
|
90
|
+
|
|
91
|
+
// Entity definition for UserOrganizationRole
|
|
92
|
+
const UserOrganizationRoleEntity = {
|
|
93
|
+
__name: 'UserOrganizationRole',
|
|
94
|
+
id: { type: 'integer', primary: true, auto: true },
|
|
95
|
+
role: { type: 'string', nullable: false },
|
|
96
|
+
// This creates a belongsTo relationship with foreignKey 'user_id'
|
|
97
|
+
User: {
|
|
98
|
+
type: 'integer',
|
|
99
|
+
relationshipType: 'belongsTo',
|
|
100
|
+
foreignKey: 'user_id',
|
|
101
|
+
foreignTable: 'User',
|
|
102
|
+
nullable: false
|
|
103
|
+
},
|
|
104
|
+
Organization: {
|
|
105
|
+
type: 'integer',
|
|
106
|
+
relationshipType: 'belongsTo',
|
|
107
|
+
foreignKey: 'organization_id',
|
|
108
|
+
foreignTable: 'Organization',
|
|
109
|
+
nullable: false
|
|
110
|
+
},
|
|
111
|
+
created_at: { type: 'string', nullable: false },
|
|
112
|
+
updated_at: { type: 'string', nullable: false }
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// User sets foreign key fields directly (common pattern)
|
|
116
|
+
const fields = {
|
|
117
|
+
user_id: '2', // ← STRING (from authService)
|
|
118
|
+
organization_id: 8, // ← NUMBER (from new entity)
|
|
119
|
+
role: 'org_admin',
|
|
120
|
+
created_at: '1770680424042',
|
|
121
|
+
updated_at: '1770680424042'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const result = engine._buildSQLInsertObjectParameterized(fields, UserOrganizationRoleEntity);
|
|
125
|
+
|
|
126
|
+
// Verify all fields are included
|
|
127
|
+
if (!result.columns.includes('[user_id]')) {
|
|
128
|
+
throw new Error('user_id field is missing from INSERT columns');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!result.columns.includes('[organization_id]')) {
|
|
132
|
+
throw new Error('organization_id field is missing from INSERT columns');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!result.columns.includes('[role]')) {
|
|
136
|
+
throw new Error('role field is missing from INSERT columns');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Verify string was converted to integer
|
|
140
|
+
const userIdIndex = result.columns.split(', ').indexOf('[user_id]');
|
|
141
|
+
if (typeof result.params[userIdIndex] !== 'number') {
|
|
142
|
+
throw new Error(`user_id should be converted to number, got ${typeof result.params[userIdIndex]}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (result.params[userIdIndex] !== 2) {
|
|
146
|
+
throw new Error(`user_id should be 2, got ${result.params[userIdIndex]}`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// TEST 2: Number value for foreign key field (should still work)
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
test('Should include number foreign key value in INSERT', () => {
|
|
155
|
+
const engine = new SimulatedSQLiteEngine();
|
|
156
|
+
|
|
157
|
+
const UserOrganizationRoleEntity = {
|
|
158
|
+
__name: 'UserOrganizationRole',
|
|
159
|
+
id: { type: 'integer', primary: true, auto: true },
|
|
160
|
+
role: { type: 'string', nullable: false },
|
|
161
|
+
User: {
|
|
162
|
+
type: 'integer',
|
|
163
|
+
relationshipType: 'belongsTo',
|
|
164
|
+
foreignKey: 'user_id',
|
|
165
|
+
foreignTable: 'User',
|
|
166
|
+
nullable: false
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const fields = {
|
|
171
|
+
user_id: 2, // ← NUMBER
|
|
172
|
+
role: 'org_admin'
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = engine._buildSQLInsertObjectParameterized(fields, UserOrganizationRoleEntity);
|
|
176
|
+
|
|
177
|
+
if (!result.columns.includes('[user_id]')) {
|
|
178
|
+
throw new Error('user_id field is missing from INSERT columns');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const userIdIndex = result.columns.split(', ').indexOf('[user_id]');
|
|
182
|
+
if (result.params[userIdIndex] !== 2) {
|
|
183
|
+
throw new Error(`user_id should be 2, got ${result.params[userIdIndex]}`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ============================================================================
|
|
188
|
+
// TEST 3: Both string and number foreign keys in same entity
|
|
189
|
+
// ============================================================================
|
|
190
|
+
|
|
191
|
+
test('Should handle mixed string and number foreign keys', () => {
|
|
192
|
+
const engine = new SimulatedSQLiteEngine();
|
|
193
|
+
|
|
194
|
+
const UserOrganizationRoleEntity = {
|
|
195
|
+
__name: 'UserOrganizationRole',
|
|
196
|
+
id: { type: 'integer', primary: true, auto: true },
|
|
197
|
+
User: {
|
|
198
|
+
type: 'integer',
|
|
199
|
+
relationshipType: 'belongsTo',
|
|
200
|
+
foreignKey: 'user_id',
|
|
201
|
+
nullable: false
|
|
202
|
+
},
|
|
203
|
+
Organization: {
|
|
204
|
+
type: 'integer',
|
|
205
|
+
relationshipType: 'belongsTo',
|
|
206
|
+
foreignKey: 'organization_id',
|
|
207
|
+
nullable: false
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const fields = {
|
|
212
|
+
user_id: '2', // STRING
|
|
213
|
+
organization_id: 8 // NUMBER
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const result = engine._buildSQLInsertObjectParameterized(fields, UserOrganizationRoleEntity);
|
|
217
|
+
|
|
218
|
+
if (!result.columns.includes('[user_id]')) {
|
|
219
|
+
throw new Error('user_id field is missing');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!result.columns.includes('[organization_id]')) {
|
|
223
|
+
throw new Error('organization_id field is missing');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Verify both are numbers
|
|
227
|
+
const userIdIndex = result.columns.split(', ').indexOf('[user_id]');
|
|
228
|
+
const orgIdIndex = result.columns.split(', ').indexOf('[organization_id]');
|
|
229
|
+
|
|
230
|
+
if (typeof result.params[userIdIndex] !== 'number') {
|
|
231
|
+
throw new Error('user_id should be a number');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (typeof result.params[orgIdIndex] !== 'number') {
|
|
235
|
+
throw new Error('organization_id should be a number');
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// TEST 4: String foreign key with leading zeros
|
|
241
|
+
// ============================================================================
|
|
242
|
+
|
|
243
|
+
test('Should handle string foreign key with leading zeros', () => {
|
|
244
|
+
const engine = new SimulatedSQLiteEngine();
|
|
245
|
+
|
|
246
|
+
const EntityWithFK = {
|
|
247
|
+
__name: 'TestEntity',
|
|
248
|
+
User: {
|
|
249
|
+
type: 'integer',
|
|
250
|
+
relationshipType: 'belongsTo',
|
|
251
|
+
foreignKey: 'user_id',
|
|
252
|
+
nullable: false
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const fields = {
|
|
257
|
+
user_id: '007' // STRING with leading zeros
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const result = engine._buildSQLInsertObjectParameterized(fields, EntityWithFK);
|
|
261
|
+
|
|
262
|
+
const userIdIndex = result.columns.split(', ').indexOf('[user_id]');
|
|
263
|
+
if (result.params[userIdIndex] !== 7) {
|
|
264
|
+
throw new Error(`Should convert '007' to 7, got ${result.params[userIdIndex]}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ============================================================================
|
|
269
|
+
// TEST 5: Invalid string value should throw error
|
|
270
|
+
// ============================================================================
|
|
271
|
+
|
|
272
|
+
test('Should throw error for non-numeric string foreign key', () => {
|
|
273
|
+
const engine = new SimulatedSQLiteEngine();
|
|
274
|
+
|
|
275
|
+
const EntityWithFK = {
|
|
276
|
+
__name: 'TestEntity',
|
|
277
|
+
User: {
|
|
278
|
+
type: 'integer',
|
|
279
|
+
relationshipType: 'belongsTo',
|
|
280
|
+
foreignKey: 'user_id',
|
|
281
|
+
nullable: false
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const fields = {
|
|
286
|
+
user_id: 'invalid' // Non-numeric string
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
engine._buildSQLInsertObjectParameterized(fields, EntityWithFK);
|
|
291
|
+
throw new Error('Should have thrown error for invalid string');
|
|
292
|
+
} catch (error) {
|
|
293
|
+
if (!error.message.includes('Cannot convert')) {
|
|
294
|
+
throw new Error(`Wrong error message: ${error.message}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// TEST 6: Empty string should throw error
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
test('Should throw error for empty string foreign key', () => {
|
|
304
|
+
const engine = new SimulatedSQLiteEngine();
|
|
305
|
+
|
|
306
|
+
const EntityWithFK = {
|
|
307
|
+
__name: 'TestEntity',
|
|
308
|
+
User: {
|
|
309
|
+
type: 'integer',
|
|
310
|
+
relationshipType: 'belongsTo',
|
|
311
|
+
foreignKey: 'user_id',
|
|
312
|
+
nullable: false
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const fields = {
|
|
317
|
+
user_id: '' // Empty string
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
engine._buildSQLInsertObjectParameterized(fields, EntityWithFK);
|
|
322
|
+
throw new Error('Should have thrown error for empty string');
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (!error.message.includes('Cannot convert')) {
|
|
325
|
+
throw new Error(`Wrong error message: ${error.message}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// TEST 7: Backward compatibility - navigation property still works
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
test('Should still work when navigation property is set (backward compat)', () => {
|
|
335
|
+
const engine = new SimulatedSQLiteEngine();
|
|
336
|
+
|
|
337
|
+
const UserOrganizationRoleEntity = {
|
|
338
|
+
__name: 'UserOrganizationRole',
|
|
339
|
+
User: {
|
|
340
|
+
type: 'integer',
|
|
341
|
+
relationshipType: 'belongsTo',
|
|
342
|
+
foreignKey: 'user_id',
|
|
343
|
+
nullable: false
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Old pattern: Set navigation property (not the foreign key)
|
|
348
|
+
const fields = {
|
|
349
|
+
User: 2 // Setting the navigation property name
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const result = engine._buildSQLInsertObjectParameterized(fields, UserOrganizationRoleEntity);
|
|
353
|
+
|
|
354
|
+
if (!result.columns.includes('[user_id]')) {
|
|
355
|
+
throw new Error('user_id field is missing');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// ============================================================================
|
|
360
|
+
// TEST 8: Prefer navigation property over foreign key field
|
|
361
|
+
// ============================================================================
|
|
362
|
+
|
|
363
|
+
test('Should prefer navigation property if both are set', () => {
|
|
364
|
+
const engine = new SimulatedSQLiteEngine();
|
|
365
|
+
|
|
366
|
+
const UserOrganizationRoleEntity = {
|
|
367
|
+
__name: 'UserOrganizationRole',
|
|
368
|
+
User: {
|
|
369
|
+
type: 'integer',
|
|
370
|
+
relationshipType: 'belongsTo',
|
|
371
|
+
foreignKey: 'user_id',
|
|
372
|
+
nullable: false
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
// Both navigation property AND foreign key set (unusual but possible)
|
|
377
|
+
const fields = {
|
|
378
|
+
User: 5, // Navigation property
|
|
379
|
+
user_id: '2' // Foreign key field
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const result = engine._buildSQLInsertObjectParameterized(fields, UserOrganizationRoleEntity);
|
|
383
|
+
|
|
384
|
+
const userIdIndex = result.columns.split(', ').indexOf('[user_id]');
|
|
385
|
+
// Should prefer navigation property value (5) over foreign key field value ('2')
|
|
386
|
+
if (result.params[userIdIndex] !== 5) {
|
|
387
|
+
throw new Error(`Should use navigation property value 5, got ${result.params[userIdIndex]}`);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ============================================================================
|
|
392
|
+
// RESULTS
|
|
393
|
+
// ============================================================================
|
|
394
|
+
|
|
395
|
+
console.log("\n" + "=".repeat(70));
|
|
396
|
+
console.log(`Tests Passed: ${passed}`);
|
|
397
|
+
console.log(`Tests Failed: ${failed}`);
|
|
398
|
+
console.log("=".repeat(70));
|
|
399
|
+
|
|
400
|
+
if (failed > 0) {
|
|
401
|
+
console.log("\n❌ Some tests failed!\n");
|
|
402
|
+
process.exit(1);
|
|
403
|
+
} else {
|
|
404
|
+
console.log("\n✅ All tests passed! Foreign key string value bug is fixed.\n");
|
|
405
|
+
process.exit(0);
|
|
406
|
+
}
|