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.
@@ -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
+ }