masterrecord 0.3.33 → 0.3.35

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.
@@ -59,7 +59,8 @@
59
59
  "Bash(npm link)",
60
60
  "Bash(npm link:*)",
61
61
  "Bash(1)",
62
- "Bash(masterrecord update-database:*)"
62
+ "Bash(masterrecord update-database:*)",
63
+ "Bash(npx mocha:*)"
63
64
  ],
64
65
  "deny": [],
65
66
  "ask": []
@@ -0,0 +1,138 @@
1
+ # Future Improvements for MasterRecord
2
+
3
+ ## Metadata Property Handling
4
+
5
+ ### Context
6
+ The `.index()` bug fix (commit 6e774a7) introduced a blacklist approach to skip metadata properties during schema processing. While this solves the immediate problem, there are architectural improvements to consider for future versions.
7
+
8
+ ### Current Solution (v0.3.33)
9
+ ```javascript
10
+ for (var key in table) {
11
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
12
+ if(key === 'indexes' || key.startsWith('__')){
13
+ continue;
14
+ }
15
+ // ... process columns
16
+ }
17
+ ```
18
+
19
+ **Status:** āœ… Shipped - Works well, well-tested, solves the problem
20
+
21
+ ### Future Considerations
22
+
23
+ #### 1. Document the Metadata Convention
24
+ **Priority:** Medium
25
+ **Effort:** Low
26
+
27
+ Add clear documentation explaining that properties starting with `__` are reserved for internal metadata and will be skipped during schema processing. This should be documented in:
28
+ - README.md (Entity Model section)
29
+ - Code comments in `entityModel.js`
30
+ - Migration guide if breaking changes are made
31
+
32
+ **Example documentation:**
33
+ ```markdown
34
+ ## Reserved Property Names
35
+ - Properties starting with `__` (double underscore) are reserved for internal metadata
36
+ - `indexes` property is reserved for index definitions
37
+ - These properties are automatically skipped during table creation
38
+ ```
39
+
40
+ #### 2. Whitelist Approach (Long-term)
41
+ **Priority:** Low
42
+ **Effort:** Medium
43
+ **Breaking Change:** Potentially
44
+
45
+ Instead of blacklisting specific properties, consider only processing properties that are actually column definitions.
46
+
47
+ **Pros:**
48
+ - More robust if new metadata properties are added
49
+ - Explicit about what gets processed
50
+ - Self-documenting code
51
+
52
+ **Cons:**
53
+ - Requires careful design to not break existing functionality
54
+ - More complex validation logic
55
+ - Could impact performance
56
+
57
+ **Example approach:**
58
+ ```javascript
59
+ for (var key in table) {
60
+ if(typeof table[key] !== "object") continue;
61
+
62
+ var col = table[key];
63
+
64
+ // Only process objects that have column-like properties
65
+ if(!col.name || !col.type) continue;
66
+
67
+ // Skip relationship types
68
+ if(col.type === 'hasOne' || col.type === 'hasMany' || col.type === 'hasManyThrough'){
69
+ continue;
70
+ }
71
+
72
+ queryVar += `${this.#columnMapping(col)}, `;
73
+ }
74
+ ```
75
+
76
+ #### 3. Use JavaScript Symbols for Metadata (v1.0+)
77
+ **Priority:** Low
78
+ **Effort:** High
79
+ **Breaking Change:** Yes
80
+
81
+ For future major versions, consider using JavaScript Symbols for all internal metadata. Symbols don't appear in `for...in` loops or `Object.keys()`, eliminating this entire class of bugs.
82
+
83
+ **Benefits:**
84
+ - Metadata becomes truly invisible to iteration
85
+ - No special-case filtering needed
86
+ - Cleaner separation of concerns
87
+ - More idiomatic JavaScript
88
+
89
+ **Migration path:**
90
+ ```javascript
91
+ // Current (v0.x)
92
+ table.__name = 'Users';
93
+ table.indexes = ['idx_name'];
94
+
95
+ // Future (v1.0+)
96
+ const META_NAME = Symbol('name');
97
+ const META_INDEXES = Symbol('indexes');
98
+
99
+ table[META_NAME] = 'Users';
100
+ table[META_INDEXES] = ['idx_name'];
101
+ ```
102
+
103
+ **Challenges:**
104
+ - Requires refactoring all metadata access
105
+ - Breaks existing code that accesses `__name`, `__compositeIndexes`, etc.
106
+ - Would need comprehensive migration guide
107
+ - Testing burden is significant
108
+
109
+ **When to consider:**
110
+ - During a major version bump (v1.0)
111
+ - When other breaking changes are planned
112
+ - After gathering user feedback on current metadata usage patterns
113
+
114
+ ---
115
+
116
+ ## Implementation Notes
117
+
118
+ ### Current Fix is Good to Ship āœ…
119
+ The blacklist approach (`indexes` and `__*` properties) is:
120
+ - Practical and solves the immediate problem
121
+ - Well-tested (5 comprehensive tests)
122
+ - Easy to understand and maintain
123
+ - Not over-engineered
124
+
125
+ ### Recommendation
126
+ 1. **Ship the current fix** (already committed)
127
+ 2. **Add documentation** about the `__` convention in next minor release
128
+ 3. **Track whitelist approach** as a potential v0.4+ enhancement
129
+ 4. **Consider Symbols** only for v1.0 major version
130
+
131
+ ---
132
+
133
+ ## Related Issues
134
+ - Index bug fix: commit 6e774a7
135
+ - Test suite: `test/index-bug-fix-test.js`
136
+
137
+ ## Feedback Credit
138
+ These suggestions came from code review feedback on the index bug fix implementation.
@@ -1,5 +1,6 @@
1
1
  /*
2
2
 
3
+ Supported column types:
3
4
  :binary
4
5
  :boolean
5
6
  :date
@@ -15,6 +16,11 @@
15
16
  :time
16
17
  :timestamp
17
18
 
19
+ Reserved Property Names:
20
+ - Properties starting with '__' (double underscore) are reserved for internal metadata
21
+ - 'indexes' property is reserved for index definitions
22
+ - These properties are automatically skipped during table creation in migration queries
23
+
18
24
  */
19
25
 
20
26
  // version 0.0.5
@@ -115,6 +121,17 @@ class EntityModel {
115
121
 
116
122
  }
117
123
 
124
+ /**
125
+ * Adds an index to this column definition.
126
+ *
127
+ * Note: The 'indexes' property is metadata and will be automatically skipped
128
+ * during table creation. Properties starting with '__' are also reserved for
129
+ * internal metadata and filtered during schema processing.
130
+ *
131
+ * @param {string} indexName - Optional custom name for the index. If not provided,
132
+ * a default name will be generated.
133
+ * @returns {EntityModel} Returns this for method chaining
134
+ */
118
135
  index(indexName){
119
136
  if(!this.obj.indexes){
120
137
  this.obj.indexes = [];
@@ -199,9 +199,16 @@ class migrationMySQLQuery {
199
199
  }
200
200
 
201
201
  if(typeof table[key] === "object"){
202
+ var col = table[key];
203
+
204
+ if(col.type !== "hasOne" && col.type !== "hasMany" && col.type !== "hasManyThrough"){
205
+ // Whitelist: Only process objects that look like column definitions
206
+ // Valid columns must have 'name' and 'type' properties
207
+ if(!col.name || !col.type){
208
+ continue;
209
+ }
202
210
 
203
- if(table[key].type !== "hasOne" && table[key].type !== "hasMany" && table[key].type !== "hasManyThrough"){
204
- queryVar += `${this.#columnMapping(table[key])}, `;
211
+ queryVar += `${this.#columnMapping(col)}, `;
205
212
  }
206
213
  }
207
214
  }
@@ -202,8 +202,16 @@ class migrationPostgresQuery {
202
202
  }
203
203
 
204
204
  if(typeof table[key] === "object"){
205
- if(table[key].type !== "hasOne" && table[key].type !== "hasMany" && table[key].type !== "hasManyThrough"){
206
- queryVar += `${this.#columnMapping(table[key])}, `;
205
+ var col = table[key];
206
+
207
+ if(col.type !== "hasOne" && col.type !== "hasMany" && col.type !== "hasManyThrough"){
208
+ // Whitelist: Only process objects that look like column definitions
209
+ // Valid columns must have 'name' and 'type' properties
210
+ if(!col.name || !col.type){
211
+ continue;
212
+ }
213
+
214
+ queryVar += `${this.#columnMapping(col)}, `;
207
215
  }
208
216
  }
209
217
  }
@@ -158,6 +158,13 @@ class migrationSQLiteQuery {
158
158
  if(col.type === 'hasOne' || col.type === 'hasMany' || col.type === 'hasManyThrough'){
159
159
  continue;
160
160
  }
161
+
162
+ // Whitelist: Only process objects that look like column definitions
163
+ // Valid columns must have 'name' and 'type' properties
164
+ if(!col.name || !col.type){
165
+ continue;
166
+ }
167
+
161
168
  queryVar += `${this.#columnMapping(col)}, `;
162
169
  }
163
170
  }
@@ -94,10 +94,10 @@ module.exports = ${this.name};
94
94
  });
95
95
 
96
96
  if(type === "up"){
97
- this.#up += os.EOL + ` this.createIndex(${indexInfoStr});`
97
+ this.#up += os.EOL + ` await this.createIndex(${indexInfoStr});`
98
98
  }
99
99
  else{
100
- this.#down += os.EOL + ` this.dropIndex(${indexInfoStr});`
100
+ this.#down += os.EOL + ` await this.dropIndex(${indexInfoStr});`
101
101
  }
102
102
  }
103
103
 
@@ -113,10 +113,10 @@ module.exports = ${this.name};
113
113
  });
114
114
 
115
115
  if(type === "up"){
116
- this.#up += os.EOL + ` this.dropIndex(${indexInfoStr});`
116
+ this.#up += os.EOL + ` await this.dropIndex(${indexInfoStr});`
117
117
  }
118
118
  else{
119
- this.#down += os.EOL + ` this.createIndex(${indexInfoStr});`
119
+ this.#down += os.EOL + ` await this.createIndex(${indexInfoStr});`
120
120
  }
121
121
  }
122
122
 
@@ -129,10 +129,10 @@ module.exports = ${this.name};
129
129
  });
130
130
 
131
131
  if(type === "up"){
132
- this.#up += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
132
+ this.#up += os.EOL + ` await this.createCompositeIndex(${indexInfoStr});`
133
133
  }
134
134
  else{
135
- this.#down += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
135
+ this.#down += os.EOL + ` await this.dropCompositeIndex(${indexInfoStr});`
136
136
  }
137
137
  }
138
138
 
@@ -145,10 +145,10 @@ module.exports = ${this.name};
145
145
  });
146
146
 
147
147
  if(type === "up"){
148
- this.#up += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
148
+ this.#up += os.EOL + ` await this.dropCompositeIndex(${indexInfoStr});`
149
149
  }
150
150
  else{
151
- this.#down += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
151
+ this.#down += os.EOL + ` await this.createCompositeIndex(${indexInfoStr});`
152
152
  }
153
153
  }
154
154
 
@@ -187,7 +187,7 @@ module.exports = ${this.name};
187
187
 
188
188
  this.#up += os.EOL + ` ];`;
189
189
  this.#up += os.EOL + ` for (const record of factoryRecords) {`;
190
- this.#up += os.EOL + ` await table.${tableName}.create(record);`;
190
+ this.#up += os.EOL + ` this.seed('${tableName}', record);`;
191
191
  this.#up += os.EOL + ` }`;
192
192
  } else {
193
193
  // Standard individual inserts for non-factory or small batches
@@ -214,10 +214,10 @@ module.exports = ${this.name};
214
214
  const formattedRecord = JSON.stringify(cleanRecord, null, 12)
215
215
  .split('\n')
216
216
  .join(os.EOL + ' ');
217
- this.#up += os.EOL + ` await table.${tableName}.create(${formattedRecord});`;
217
+ this.#up += os.EOL + ` this.seed('${tableName}', ${formattedRecord});`;
218
218
  } else {
219
219
  // Single-line format
220
- this.#up += os.EOL + ` await table.${tableName}.create(${recordStr});`;
220
+ this.#up += os.EOL + ` this.seed('${tableName}', ${recordStr});`;
221
221
  }
222
222
  }
223
223
  });
@@ -236,7 +236,7 @@ module.exports = ${this.name};
236
236
  }
237
237
 
238
238
  this.#up += os.EOL + ` {`;
239
- this.#up += os.EOL + ` const existing = await table.${tableName}.where(r => r.${conflictKey} == ${JSON.stringify(conflictValue)}).single();`;
239
+ this.#up += os.EOL + ` const existing = await this.context.${tableName}.where(r => r.${conflictKey} == ${JSON.stringify(conflictValue)}).single();`;
240
240
  this.#up += os.EOL + ` if (existing) {`;
241
241
 
242
242
  // Update logic
@@ -257,7 +257,7 @@ module.exports = ${this.name};
257
257
 
258
258
  this.#up += os.EOL + ` await existing.save();`;
259
259
  this.#up += os.EOL + ` } else {`;
260
- this.#up += os.EOL + ` await table.${tableName}.create(${JSON.stringify(cleanRecord)});`;
260
+ this.#up += os.EOL + ` this.seed('${tableName}', ${JSON.stringify(cleanRecord)});`;
261
261
  this.#up += os.EOL + ` }`;
262
262
  this.#up += os.EOL + ` }`;
263
263
  }
@@ -15,10 +15,21 @@ class Migrations{
15
15
 
16
16
  #organizeSchemaByTables(oldSchema, newSchema){
17
17
  var tables = []
18
+ var seenTableNames = new Set(); // Track processed table names to prevent duplicates
19
+
18
20
  if(oldSchema.length === 0){
19
21
  newSchema.forEach(function (item, index) {
22
+ var tableName = item["__name"];
23
+
24
+ // Skip if we've already processed this table name
25
+ if(seenTableNames.has(tableName)){
26
+ console.warn(`Warning: Duplicate table definition detected for "${tableName}" - using first occurrence only`);
27
+ return;
28
+ }
29
+ seenTableNames.add(tableName);
30
+
20
31
  var table = {
21
- name: item["__name"],
32
+ name: tableName,
22
33
  new :item,
23
34
  old : {},
24
35
  newColumns : [],
@@ -36,8 +47,17 @@ class Migrations{
36
47
  }
37
48
  else{
38
49
  newSchema.forEach(function (item, index) {
50
+ var tableName = item["__name"];
51
+
52
+ // Skip if we've already processed this table name
53
+ if(seenTableNames.has(tableName)){
54
+ console.warn(`Warning: Duplicate table definition detected for "${tableName}" - using first occurrence only`);
55
+ return;
56
+ }
57
+ seenTableNames.add(tableName);
58
+
39
59
  var table = {
40
- name: item["__name"],
60
+ name: tableName,
41
61
  old: null,
42
62
  new :item,
43
63
  newColumns : [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.33",
3
+ "version": "0.3.35",
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": {
package/readme.md CHANGED
@@ -3367,11 +3367,73 @@ if (ids.length > 0) {
3367
3367
  user.name = null; // Error if name is { nullable: false }
3368
3368
  ```
3369
3369
 
3370
+ ## Changelog
3371
+
3372
+ ### Version 0.3.35 (2026-02-05) - CRITICAL FIX
3373
+
3374
+ #### Critical Bug Fix
3375
+ - **FIXED**: Duplicate table operations in migrations when snapshots contain duplicate table definitions
3376
+ - **Root Cause**: If a context registered the same entity twice (via multiple `dbset()` calls), the snapshot would contain duplicate table definitions with the same `__name`
3377
+ - **Impact**: Migrations would generate duplicate `createTable()` and `seed()` calls, causing duplicate records in the database
3378
+ - **Examples**:
3379
+ - ragContext: Settings table appeared twice → 2x seed insertions
3380
+ - qaContext: TaxonomyTemplate appeared twice → 18 template records instead of 9
3381
+ - **Fix**: Added deduplication in `#organizeSchemaByTables()` using a `Set` to track processed table names
3382
+ - **User Impact**: If you ran migrations from v0.3.34 or earlier and have duplicate records:
3383
+ 1. Manually remove duplicates from your database
3384
+ 2. Regenerate migrations with v0.3.35: `masterrecord add-migration YourContext "regenerate"`
3385
+ 3. Future migrations will not create duplicates
3386
+
3387
+ #### Technical Details
3388
+ - Modified `Migrations/migrations.js` - `#organizeSchemaByTables()` method
3389
+ - Added `seenTableNames` Set to track processed tables and prevent duplicates
3390
+ - Emits warning: `"Warning: Duplicate table definition detected for 'TableName' - using first occurrence only"`
3391
+ - Works for both initial migrations (`oldSchema.length === 0`) and subsequent migrations
3392
+
3393
+ #### Testing
3394
+ - Added 2 new tests for duplicate table scenarios (13 total tests)
3395
+ - All tests passing with duplicate detection and deduplication verified
3396
+
3397
+ ### Version 0.3.34 (2026-02-05) - PARTIALLY EFFECTIVE
3398
+
3399
+ āš ļø **Note**: This version fixed the seed API bug but did NOT fix duplicate operations. Use v0.3.35 instead.
3400
+
3401
+ #### Bug Fixes
3402
+ - **Fixed**: Seed API migration generation - resolved `table.EntityName.create is not a function` error
3403
+ - Migrations now use `this.seed('TableName', data)` instead of `table.TableName.create(data)`
3404
+ - Ensures compatibility with the migration schema base class
3405
+ - **Fixed**: Missing `await` keywords on `createIndex()`, `dropIndex()`, `createCompositeIndex()`, and `dropCompositeIndex()` calls in migrations
3406
+ - All async index operations now properly awaited for consistency
3407
+ - ~~**Fixed**: Eliminated duplicate table creation statements~~ āŒ **NOT ACTUALLY FIXED** - see v0.3.35
3408
+ - ~~**Fixed**: Eliminated duplicate seed insertion statements~~ āŒ **NOT ACTUALLY FIXED** - see v0.3.35
3409
+
3410
+ #### Improvements
3411
+ - **Enhanced**: Query builders now use whitelist validation for column definitions
3412
+ - Validates that objects have both `name` and `type` properties before processing as columns
3413
+ - More robust metadata property filtering in schema processing
3414
+ - Combines blacklist (skip `indexes`, `__*` prefixed) with whitelist (require `name` and `type`) for comprehensive validation
3415
+ - **Documentation**: Enhanced migration generation documentation
3416
+ - **Testing**: Added comprehensive test suite for v0.3.34 bug fixes
3417
+ - Tests whitelist validation across all three database backends
3418
+ - Tests async/await consistency in index operations
3419
+ - Tests seed API correctness and deduplication
3420
+
3421
+ #### Technical Details
3422
+ - Query builders (SQLite, MySQL, PostgreSQL) validate column definitions have required `name` and `type` properties
3423
+ - Migration template generates proper ORM API calls for seed data using `this.seed()` method
3424
+ - Index deduplication logic prevents duplicate CREATE INDEX statements
3425
+ - All migration operations consistently use `await` for async consistency
3426
+
3427
+ #### Migration Notes
3428
+ - Existing migrations generated with older versions will continue to work
3429
+ - New migrations will use the corrected seed API syntax
3430
+ - If you have migrations with `table.EntityName.create()` that haven't been run, regenerate them with this version
3431
+
3370
3432
  ## Version Compatibility
3371
3433
 
3372
3434
  | Component | Version | Notes |
3373
3435
  |---------------|---------------|------------------------------------------|
3374
- | MasterRecord | 0.3.13 | Current version with PostgreSQL support |
3436
+ | MasterRecord | 0.3.35 | Current version - fixes critical duplicate table bug |
3375
3437
  | Node.js | 14+ | Async/await support required |
3376
3438
  | PostgreSQL | 9.6+ (12+) | Tested with 12, 13, 14, 15, 16 |
3377
3439
  | MySQL | 5.7+ (8.0+) | Tested with 8.0+ |
@@ -0,0 +1,386 @@
1
+ /**
2
+ * Comprehensive Tests for v0.3.34 Bug Fixes
3
+ *
4
+ * Tests:
5
+ * 1. Whitelist validation in query builders
6
+ * 2. No duplicate index creation
7
+ * 3. Seed API migration generation (no duplicates, correct API usage)
8
+ * 4. All async operations have await keywords
9
+ */
10
+
11
+ console.log("╔════════════════════════════════════════════════════════════════╗");
12
+ console.log("ā•‘ MasterRecord v0.3.34 Bug Fixes Test ā•‘");
13
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
14
+
15
+ const Migrations = require('../Migrations/migrations');
16
+ const MigrationSQLiteQuery = require('../Migrations/migrationSQLiteQuery');
17
+ const MigrationMySQLQuery = require('../Migrations/migrationMySQLQuery');
18
+ const MigrationPostgresQuery = require('../Migrations/migrationPostgresQuery');
19
+
20
+ let passed = 0;
21
+ let failed = 0;
22
+
23
+ function assert(condition, message) {
24
+ if (!condition) {
25
+ console.log(`āŒ FAIL: ${message}`);
26
+ failed++;
27
+ return false;
28
+ }
29
+ return true;
30
+ }
31
+
32
+ function test(description, fn) {
33
+ try {
34
+ fn();
35
+ console.log(`āœ“ ${description}`);
36
+ passed++;
37
+ } catch (e) {
38
+ console.log(`āŒ FAIL: ${description}`);
39
+ console.log(` Error: ${e.message}`);
40
+ failed++;
41
+ }
42
+ }
43
+
44
+ // =============================================================================
45
+ // Test Suite 1: Whitelist Validation
46
+ // =============================================================================
47
+ console.log("\nšŸ“‹ Test Suite 1: Whitelist Validation in Query Builders\n");
48
+
49
+ test('SQLite query builder skips objects without name property', () => {
50
+ const queryBuilder = new MigrationSQLiteQuery();
51
+ const table = {
52
+ __name: 'TestTable',
53
+ id: { name: 'id', type: 'integer', primaryKey: true },
54
+ weirdMetadata: { foo: 'bar' }, // No name or type - should be skipped
55
+ name: { name: 'name', type: 'string' }
56
+ };
57
+
58
+ const query = queryBuilder.createTable(table);
59
+
60
+ assert(!query.includes('undefined'), 'SQLite: Should not contain undefined');
61
+ assert(query.includes('id'), 'SQLite: Should contain id column');
62
+ assert(query.includes('name'), 'SQLite: Should contain name column');
63
+ assert(!query.includes('foo'), 'SQLite: Should not process metadata without name/type');
64
+ });
65
+
66
+ test('MySQL query builder skips objects without name property', () => {
67
+ const queryBuilder = new MigrationMySQLQuery();
68
+ const table = {
69
+ __name: 'TestTable',
70
+ id: { name: 'id', type: 'integer', primaryKey: true },
71
+ weirdMetadata: { foo: 'bar' },
72
+ name: { name: 'name', type: 'string' }
73
+ };
74
+
75
+ const query = queryBuilder.createTable(table);
76
+
77
+ assert(!query.includes('undefined'), 'MySQL: Should not contain undefined');
78
+ assert(query.includes('id'), 'MySQL: Should contain id column');
79
+ assert(query.includes('name'), 'MySQL: Should contain name column');
80
+ });
81
+
82
+ test('PostgreSQL query builder skips objects without name property', () => {
83
+ const queryBuilder = new MigrationPostgresQuery();
84
+ const table = {
85
+ __name: 'TestTable',
86
+ id: { name: 'id', type: 'integer', primaryKey: true },
87
+ weirdMetadata: { foo: 'bar' },
88
+ name: { name: 'name', type: 'string' }
89
+ };
90
+
91
+ const query = queryBuilder.createTable(table);
92
+
93
+ assert(!query.includes('undefined'), 'PostgreSQL: Should not contain undefined');
94
+ assert(query.includes('id'), 'PostgreSQL: Should contain id column');
95
+ assert(query.includes('name'), 'PostgreSQL: Should contain name column');
96
+ });
97
+
98
+ test('Query builders correctly skip indexes property', () => {
99
+ const queryBuilder = new MigrationSQLiteQuery();
100
+ const table = {
101
+ __name: 'TestTable',
102
+ id: { name: 'id', type: 'integer', primaryKey: true },
103
+ email: { name: 'email', type: 'string', indexes: ['idx_email'] },
104
+ indexes: ['some', 'array'] // This is metadata, should be skipped
105
+ };
106
+
107
+ const query = queryBuilder.createTable(table);
108
+
109
+ assert(!query.includes('undefined'), 'Should not contain undefined');
110
+ assert(query.includes('email'), 'Should contain email column');
111
+ assert(!query.match(/some.*NOT NULL/), 'Should not process indexes array as column');
112
+ });
113
+
114
+ // =============================================================================
115
+ // Test Suite 2: Index Creation with Await
116
+ // =============================================================================
117
+ console.log("\nšŸ“‹ Test Suite 2: Index Creation with Await Keywords\n");
118
+
119
+ test('createIndex generates code with await keyword', () => {
120
+ const migrations = new Migrations();
121
+ const oldSchema = [];
122
+ const newSchema = [{
123
+ __name: 'User',
124
+ id: { name: 'id', type: 'integer', primaryKey: true },
125
+ email: { name: 'email', type: 'text', indexes: ['idx_user_email'] }
126
+ }];
127
+
128
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema);
129
+
130
+ const createIndexMatches = migrationCode.match(/await this\.createIndex/g);
131
+ assert(createIndexMatches && createIndexMatches.length > 0, 'createIndex should have await keyword');
132
+ assert(!migrationCode.match(/\n\s+this\.createIndex\(/), 'Should not have createIndex without await');
133
+ });
134
+
135
+ test('createCompositeIndex generates code with await keyword', () => {
136
+ const migrations = new Migrations();
137
+ const oldSchema = [];
138
+ const newSchema = [{
139
+ __name: 'User',
140
+ id: { name: 'id', type: 'integer' },
141
+ __compositeIndexes: [{
142
+ name: 'idx_composite',
143
+ columns: ['col1', 'col2'],
144
+ unique: false
145
+ }]
146
+ }];
147
+
148
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema);
149
+
150
+ const compositeMatches = migrationCode.match(/await this\.createCompositeIndex/g);
151
+ assert(compositeMatches && compositeMatches.length > 0, 'createCompositeIndex should have await keyword');
152
+ });
153
+
154
+ // =============================================================================
155
+ // Test Suite 3: Seed API Migration Generation
156
+ // =============================================================================
157
+ console.log("\nšŸ“‹ Test Suite 3: Seed API Migration Generation\n");
158
+
159
+ test('Seed data uses correct API (this.seed instead of table.EntityName.create)', () => {
160
+ const migrations = new Migrations();
161
+ const oldSchema = [];
162
+ const newSchema = [{
163
+ __name: 'Settings',
164
+ id: { name: 'id', type: 'integer', primaryKey: true },
165
+ disable_rag: { name: 'disable_rag', type: 'integer' }
166
+ }];
167
+ const seedData = {
168
+ Settings: [{ disable_rag: 0 }]
169
+ };
170
+
171
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema, seedData);
172
+
173
+ assert(migrationCode.includes("this.seed('Settings'"), 'Should use this.seed() method');
174
+ assert(!migrationCode.includes('table.Settings.create'), 'Should NOT use table.Settings.create()');
175
+ });
176
+
177
+ test('No duplicate createTable calls for tables with seed data', () => {
178
+ const migrations = new Migrations();
179
+ const oldSchema = [];
180
+ const newSchema = [
181
+ {
182
+ __name: 'Document',
183
+ id: { name: 'id', type: 'integer', primaryKey: true }
184
+ },
185
+ {
186
+ __name: 'Settings',
187
+ id: { name: 'id', type: 'integer', primaryKey: true }
188
+ }
189
+ ];
190
+ const seedData = {
191
+ Settings: [{ id: 1 }]
192
+ };
193
+
194
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema, seedData);
195
+
196
+ const settingsCreateMatches = migrationCode.match(/createTable\(table\.Settings\)/g);
197
+ const settingsCreateCount = settingsCreateMatches ? settingsCreateMatches.length : 0;
198
+
199
+ assert(settingsCreateCount === 1, `Expected 1 createTable for Settings, got ${settingsCreateCount}`);
200
+ });
201
+
202
+ test('No duplicate seed insertion calls', () => {
203
+ const migrations = new Migrations();
204
+ const oldSchema = [];
205
+ const newSchema = [{
206
+ __name: 'Settings',
207
+ id: { name: 'id', type: 'integer', primaryKey: true }
208
+ }];
209
+ const seedData = {
210
+ Settings: [{ id: 1, value: 'test' }]
211
+ };
212
+
213
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema, seedData);
214
+
215
+ const seedMatches = migrationCode.match(/this\.seed\('Settings'/g);
216
+ const seedCount = seedMatches ? seedMatches.length : 0;
217
+
218
+ assert(seedCount === 1, `Expected 1 seed call, got ${seedCount}`);
219
+ });
220
+
221
+ test('No duplicate dropTable calls in down migration', () => {
222
+ const migrations = new Migrations();
223
+ const oldSchema = [];
224
+ const newSchema = [{
225
+ __name: 'Settings',
226
+ id: { name: 'id', type: 'integer', primaryKey: true }
227
+ }];
228
+ const seedData = {
229
+ Settings: [{ id: 1 }]
230
+ };
231
+
232
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema, seedData);
233
+
234
+ const dropMatches = migrationCode.match(/dropTable\(table\.Settings\)/g);
235
+ const dropCount = dropMatches ? dropMatches.length : 0;
236
+
237
+ assert(dropCount === 1, `Expected 1 dropTable call, got ${dropCount}`);
238
+ });
239
+
240
+ test('Complex scenario: Multiple tables with seed data, indexes, and composite indexes', () => {
241
+ const migrations = new Migrations();
242
+ const oldSchema = [];
243
+ const newSchema = [
244
+ {
245
+ __name: 'Document',
246
+ id: { name: 'id', type: 'integer', primaryKey: true },
247
+ embedding_model_id: { name: 'embedding_model_id', type: 'integer', indexes: ['idx_doc_embed'] }
248
+ },
249
+ {
250
+ __name: 'DocumentChunk',
251
+ id: { name: 'id', type: 'integer', primaryKey: true },
252
+ __compositeIndexes: [{
253
+ name: 'idx_chunk_unique',
254
+ columns: ['doc_id', 'chunk_index'],
255
+ unique: true
256
+ }]
257
+ },
258
+ {
259
+ __name: 'Settings',
260
+ id: { name: 'id', type: 'integer', primaryKey: true },
261
+ disable_rag: { name: 'disable_rag', type: 'integer' }
262
+ },
263
+ {
264
+ __name: 'Job',
265
+ id: { name: 'id', type: 'integer', primaryKey: true }
266
+ }
267
+ ];
268
+ const seedData = {
269
+ Settings: [{ disable_rag: 0 }]
270
+ };
271
+
272
+ const migrationCode = migrations.template('InitialCreate', oldSchema, newSchema, seedData);
273
+
274
+ // Check each table appears exactly once
275
+ const docCreate = (migrationCode.match(/createTable\(table\.Document\)/g) || []).length;
276
+ const chunkCreate = (migrationCode.match(/createTable\(table\.DocumentChunk\)/g) || []).length;
277
+ const settingsCreate = (migrationCode.match(/createTable\(table\.Settings\)/g) || []).length;
278
+ const jobCreate = (migrationCode.match(/createTable\(table\.Job\)/g) || []).length;
279
+
280
+ assert(docCreate === 1, `Document: Expected 1 createTable, got ${docCreate}`);
281
+ assert(chunkCreate === 1, `DocumentChunk: Expected 1 createTable, got ${chunkCreate}`);
282
+ assert(settingsCreate === 1, `Settings: Expected 1 createTable, got ${settingsCreate}`);
283
+ assert(jobCreate === 1, `Job: Expected 1 createTable, got ${jobCreate}`);
284
+
285
+ // Check seed uses correct API
286
+ assert(migrationCode.includes("this.seed('Settings'"), 'Should use this.seed() for Settings');
287
+ assert(!migrationCode.includes('table.Settings.create'), 'Should NOT use table.Settings.create');
288
+
289
+ // Check indexes have await
290
+ assert(migrationCode.includes('await this.createIndex'), 'Indexes should have await');
291
+ assert(migrationCode.includes('await this.createCompositeIndex'), 'Composite indexes should have await');
292
+ });
293
+
294
+ // =============================================================================
295
+ // Test Suite 4: Duplicate Table Deduplication (v0.3.35)
296
+ // =============================================================================
297
+ console.log("\nšŸ“‹ Test Suite 4: Duplicate Table Deduplication\n");
298
+
299
+ test('Deduplicates when snapshot has duplicate table definitions', () => {
300
+ const migrations = new Migrations();
301
+
302
+ // Simulate snapshot with duplicate Settings table (real-world bug from ragContext)
303
+ const oldSchema = [];
304
+ const newSchema = [
305
+ {
306
+ __name: 'Settings',
307
+ id: { name: 'id', type: 'integer', primaryKey: true },
308
+ disable_rag: { name: 'disable_rag', type: 'integer' }
309
+ },
310
+ {
311
+ __name: 'Document',
312
+ id: { name: 'id', type: 'integer', primaryKey: true }
313
+ },
314
+ {
315
+ __name: 'Settings', // DUPLICATE - this caused the bug
316
+ id: { name: 'id', type: 'integer', primaryKey: true },
317
+ disable_rag: { name: 'disable_rag', type: 'integer' }
318
+ }
319
+ ];
320
+
321
+ const seedData = {
322
+ Settings: [{ disable_rag: 0 }]
323
+ };
324
+
325
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema, seedData);
326
+
327
+ const createTableCount = (migrationCode.match(/createTable\(table\.Settings\)/g) || []).length;
328
+ const seedCount = (migrationCode.match(/this\.seed\('Settings'/g) || []).length;
329
+
330
+ assert(createTableCount === 1, `Expected 1 createTable for Settings, got ${createTableCount}`);
331
+ assert(seedCount === 1, `Expected 1 seed call for Settings, got ${seedCount}`);
332
+ });
333
+
334
+ test('Deduplicates multiple tables with multiple seeds (qaContext scenario)', () => {
335
+ const migrations = new Migrations();
336
+
337
+ const oldSchema = [];
338
+ const newSchema = [
339
+ {
340
+ __name: 'TaxonomyTemplate',
341
+ id: { name: 'id', type: 'integer', primaryKey: true },
342
+ template_id: { name: 'template_id', type: 'string' }
343
+ },
344
+ {
345
+ __name: 'Document',
346
+ id: { name: 'id', type: 'integer', primaryKey: true }
347
+ },
348
+ {
349
+ __name: 'TaxonomyTemplate', // DUPLICATE
350
+ id: { name: 'id', type: 'integer', primaryKey: true },
351
+ template_id: { name: 'template_id', type: 'string' }
352
+ }
353
+ ];
354
+
355
+ const seedData = {
356
+ TaxonomyTemplate: [
357
+ { template_id: 'template1' },
358
+ { template_id: 'template2' },
359
+ { template_id: 'template3' },
360
+ { template_id: 'template4' },
361
+ { template_id: 'template5' }
362
+ ]
363
+ };
364
+
365
+ const migrationCode = migrations.template('TestMigration', oldSchema, newSchema, seedData);
366
+
367
+ const createTableCount = (migrationCode.match(/createTable\(table\.TaxonomyTemplate\)/g) || []).length;
368
+ const seedCount = (migrationCode.match(/this\.seed\('TaxonomyTemplate'/g) || []).length;
369
+
370
+ assert(createTableCount === 1, `Expected 1 createTable for TaxonomyTemplate, got ${createTableCount}`);
371
+ assert(seedCount === 5, `Expected 5 seed calls for TaxonomyTemplate, got ${seedCount}`);
372
+ });
373
+
374
+ // =============================================================================
375
+ // Test Results Summary
376
+ // =============================================================================
377
+ console.log("\n" + "=".repeat(68));
378
+ console.log(`\nšŸ“Š Test Results: ${passed} passed, ${failed} failed\n`);
379
+
380
+ if (failed === 0) {
381
+ console.log("āœ… All tests passed! v0.3.34/0.3.35 fixes are working correctly.\n");
382
+ process.exit(0);
383
+ } else {
384
+ console.log(`āš ļø ${failed} test(s) failed. Please review the output above.\n`);
385
+ process.exit(1);
386
+ }
@@ -1,63 +0,0 @@
1
- /**
2
- * Debug test to trace ID setting
3
- */
4
-
5
- const masterrecord = require('../MasterRecord.js');
6
- const path = require('path');
7
- const fs = require('fs');
8
-
9
- class User {
10
- id(db) {
11
- db.integer().primary().auto();
12
- }
13
- name(db) {
14
- db.string();
15
- }
16
- }
17
-
18
- class TestContext extends masterrecord.context {
19
- constructor() {
20
- super();
21
- this.database = path.join(__dirname, '..', 'database', 'debugIdTest.db');
22
- }
23
- onConfig(db) {
24
- this.dbset(User);
25
- }
26
- }
27
-
28
- // Clean
29
- if (fs.existsSync(path.join(__dirname, '..', 'database', 'debugIdTest.db'))) {
30
- fs.unlinkSync(path.join(__dirname, '..', 'database', 'debugIdTest.db'));
31
- }
32
-
33
- async function test() {
34
- const db = new TestContext();
35
- db.onConfig();
36
-
37
- const user = db.User.new();
38
- user.name = 'Test';
39
-
40
- console.log('\n=== Manual ID set test ===');
41
- console.log('Before manual set - user.id:', user.id);
42
-
43
- user.id = 123;
44
- console.log('After user.id = 123 - user.id:', user.id);
45
- console.log('After manual set - user.__proto__._id:', user.__proto__._id);
46
-
47
- user.id = 456;
48
- console.log('After user.id = 456 - user.id:', user.id);
49
-
50
- console.log('\n=== Now test with save ===');
51
- const user2 = db.User.new();
52
- user2.name = 'Test2';
53
-
54
- console.log('Before save - tracked entities:', db.__trackedEntities.length);
55
- console.log('Before save - user2.__state:', user2.__state);
56
-
57
- await user2.save();
58
-
59
- console.log('After save - user2.id:', user2.id);
60
- console.log('After save - user2.__proto__._id:', user2.__proto__._id);
61
- }
62
-
63
- test().catch(console.error);