masterrecord 0.3.32 → 0.3.34

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.
@@ -58,7 +58,9 @@
58
58
  "Bash(wc:*)",
59
59
  "Bash(npm link)",
60
60
  "Bash(npm link:*)",
61
- "Bash(1)"
61
+ "Bash(1)",
62
+ "Bash(masterrecord update-database:*)",
63
+ "Bash(npx mocha:*)"
62
64
  ],
63
65
  "deny": [],
64
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 = [];
@@ -193,10 +193,22 @@ class migrationMySQLQuery {
193
193
  var queryVar = "";
194
194
  //console.log("Dsfdsfdsf---------", table)
195
195
  for (var key in table) {
196
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
197
+ if(key === 'indexes' || key.startsWith('__')){
198
+ continue;
199
+ }
200
+
196
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
+ }
197
210
 
198
- if(table[key].type !== "hasOne" && table[key].type !== "hasMany" && table[key].type !== "hasManyThrough"){
199
- queryVar += `${this.#columnMapping(table[key])}, `;
211
+ queryVar += `${this.#columnMapping(col)}, `;
200
212
  }
201
213
  }
202
214
  }
@@ -196,9 +196,22 @@ class migrationPostgresQuery {
196
196
  var queryVar = "";
197
197
 
198
198
  for (var key in table) {
199
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
200
+ if(key === 'indexes' || key.startsWith('__')){
201
+ continue;
202
+ }
203
+
199
204
  if(typeof table[key] === "object"){
200
- if(table[key].type !== "hasOne" && table[key].type !== "hasMany" && table[key].type !== "hasManyThrough"){
201
- 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)}, `;
202
215
  }
203
216
  }
204
217
  }
@@ -147,16 +147,28 @@ class migrationSQLiteQuery {
147
147
  createTable(table){
148
148
  var queryVar = "";
149
149
  for (var key in table) {
150
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
151
+ if(key === 'indexes' || key.startsWith('__')){
152
+ continue;
153
+ }
154
+
150
155
  if(typeof table[key] === "object"){
151
156
  var col = table[key];
152
157
  // Skip relationship-only fields
153
158
  if(col.type === 'hasOne' || col.type === 'hasMany' || col.type === 'hasManyThrough'){
154
159
  continue;
155
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
+
156
168
  queryVar += `${this.#columnMapping(col)}, `;
157
169
  }
158
170
  }
159
-
171
+
160
172
  return `CREATE TABLE IF NOT EXISTS ${table.__name} (${queryVar.replace(/,\s*$/, "")});`;
161
173
 
162
174
  /*
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
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,48 @@ 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.34 (2026-02-05)
3373
+
3374
+ #### Bug Fixes
3375
+ - **Fixed**: Seed API migration generation - resolved `table.EntityName.create is not a function` error
3376
+ - Migrations now use `this.seed('TableName', data)` instead of `table.TableName.create(data)`
3377
+ - Ensures compatibility with the migration schema base class
3378
+ - **Fixed**: Eliminated duplicate table creation statements in generated migrations
3379
+ - **Fixed**: Eliminated duplicate seed insertion statements in generated migrations
3380
+ - **Fixed**: Duplicate index creation when using `.index()` in column definitions
3381
+ - Index operations now properly deduplicated during migration generation
3382
+ - **Fixed**: Missing `await` keywords on `createIndex()`, `dropIndex()`, `createCompositeIndex()`, and `dropCompositeIndex()` calls in migrations
3383
+ - All async index operations now properly awaited for consistency
3384
+
3385
+ #### Improvements
3386
+ - **Enhanced**: Query builders now use whitelist validation for column definitions
3387
+ - Validates that objects have both `name` and `type` properties before processing as columns
3388
+ - More robust metadata property filtering in schema processing
3389
+ - Combines blacklist (skip `indexes`, `__*` prefixed) with whitelist (require `name` and `type`) for comprehensive validation
3390
+ - **Documentation**: Enhanced migration generation documentation
3391
+ - **Testing**: Added comprehensive test suite for v0.3.34 bug fixes
3392
+ - Tests whitelist validation across all three database backends
3393
+ - Tests async/await consistency in index operations
3394
+ - Tests seed API correctness and deduplication
3395
+
3396
+ #### Technical Details
3397
+ - Query builders (SQLite, MySQL, PostgreSQL) validate column definitions have required `name` and `type` properties
3398
+ - Migration template generates proper ORM API calls for seed data using `this.seed()` method
3399
+ - Index deduplication logic prevents duplicate CREATE INDEX statements
3400
+ - All migration operations consistently use `await` for async consistency
3401
+
3402
+ #### Migration Notes
3403
+ - Existing migrations generated with older versions will continue to work
3404
+ - New migrations will use the corrected seed API syntax
3405
+ - If you have migrations with `table.EntityName.create()` that haven't been run, regenerate them with this version
3406
+
3370
3407
  ## Version Compatibility
3371
3408
 
3372
3409
  | Component | Version | Notes |
3373
3410
  |---------------|---------------|------------------------------------------|
3374
- | MasterRecord | 0.3.13 | Current version with PostgreSQL support |
3411
+ | MasterRecord | 0.3.34 | Current version with bug fixes and improvements |
3375
3412
  | Node.js | 14+ | Async/await support required |
3376
3413
  | PostgreSQL | 9.6+ (12+) | Tested with 12, 13, 14, 15, 16 |
3377
3414
  | MySQL | 5.7+ (8.0+) | Tested with 8.0+ |
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Test: Index Bug Fix - createTable should not generate "undefined undefined NOT NULL"
3
+ *
4
+ * Verifies that .index() method doesn't cause malformed column definitions in CREATE TABLE statements
5
+ */
6
+
7
+ console.log("╔════════════════════════════════════════════════════════════════╗");
8
+ console.log("ā•‘ Index Bug Fix Test - createTable() Method ā•‘");
9
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
10
+
11
+ const MigrationSQLiteQuery = require('../Migrations/migrationSQLiteQuery.js');
12
+ const MigrationMySQLQuery = require('../Migrations/migrationMySQLQuery.js');
13
+ const MigrationPostgresQuery = require('../Migrations/migrationPostgresQuery.js');
14
+
15
+ let passed = 0;
16
+ let failed = 0;
17
+
18
+ function assert(condition, message) {
19
+ if (!condition) {
20
+ console.log(`āŒ FAIL: ${message}`);
21
+ failed++;
22
+ return false;
23
+ }
24
+ return true;
25
+ }
26
+
27
+ function test(description, fn) {
28
+ try {
29
+ fn();
30
+ console.log(`āœ“ ${description}`);
31
+ passed++;
32
+ } catch (error) {
33
+ console.log(`āœ— ${description}`);
34
+ console.log(` Error: ${error.message}`);
35
+ failed++;
36
+ }
37
+ }
38
+
39
+ // Mock table object with indexes property (simulates what EntityModel creates)
40
+ const mockTable = {
41
+ __name: 'TestTable',
42
+ id: {
43
+ name: 'id',
44
+ type: 'integer',
45
+ nullable: false,
46
+ primary: true,
47
+ autoIncrement: true
48
+ },
49
+ organization_id: {
50
+ name: 'organization_id',
51
+ type: 'integer',
52
+ nullable: false,
53
+ indexes: ['idx_org'] // This property on the column caused the original bug
54
+ },
55
+ name: {
56
+ name: 'name',
57
+ type: 'string',
58
+ nullable: false
59
+ },
60
+ // This is the problematic property that was being treated as a column
61
+ indexes: ['idx_org']
62
+ };
63
+
64
+ console.log("Testing SQLite Query Builder...\n");
65
+
66
+ test('SQLite createTable should skip indexes property', () => {
67
+ const sqliteQuery = new MigrationSQLiteQuery();
68
+ const sql = sqliteQuery.createTable(mockTable);
69
+
70
+ assert(!sql.includes('undefined undefined'),
71
+ `SQL should not contain "undefined undefined" but got: ${sql}`);
72
+ assert(sql.includes('id'), 'Should include id column');
73
+ assert(sql.includes('organization_id'), 'Should include organization_id column');
74
+ assert(sql.includes('name'), 'Should include name column');
75
+ assert(sql.startsWith('CREATE TABLE IF NOT EXISTS TestTable'),
76
+ 'Should start with CREATE TABLE');
77
+
78
+ console.log(` Generated SQL: ${sql}\n`);
79
+ });
80
+
81
+ console.log("Testing MySQL Query Builder...\n");
82
+
83
+ test('MySQL createTable should skip indexes property', () => {
84
+ const mysqlQuery = new MigrationMySQLQuery();
85
+ const sql = mysqlQuery.createTable(mockTable);
86
+
87
+ assert(!sql.includes('undefined undefined'),
88
+ `SQL should not contain "undefined undefined" but got: ${sql}`);
89
+ assert(sql.includes('id'), 'Should include id column');
90
+ assert(sql.includes('organization_id'), 'Should include organization_id column');
91
+ assert(sql.includes('name'), 'Should include name column');
92
+ assert(sql.startsWith('CREATE TABLE IF NOT EXISTS `TestTable`'),
93
+ 'Should start with CREATE TABLE');
94
+
95
+ console.log(` Generated SQL: ${sql}\n`);
96
+ });
97
+
98
+ console.log("Testing PostgreSQL Query Builder...\n");
99
+
100
+ test('PostgreSQL createTable should skip indexes property', () => {
101
+ const postgresQuery = new MigrationPostgresQuery();
102
+ const sql = postgresQuery.createTable(mockTable);
103
+
104
+ assert(!sql.includes('undefined undefined'),
105
+ `SQL should not contain "undefined undefined" but got: ${sql}`);
106
+ assert(sql.includes('id'), 'Should include id column');
107
+ assert(sql.includes('organization_id'), 'Should include organization_id column');
108
+ assert(sql.includes('name'), 'Should include name column');
109
+ assert(sql.startsWith('CREATE TABLE IF NOT EXISTS "TestTable"'),
110
+ 'Should start with CREATE TABLE');
111
+
112
+ console.log(` Generated SQL: ${sql}\n`);
113
+ });
114
+
115
+ console.log("Testing metadata properties filtering...\n");
116
+
117
+ test('Should skip __compositeIndexes and other __ properties', () => {
118
+ const tableWithMetadata = {
119
+ ...mockTable,
120
+ __compositeIndexes: [['col1', 'col2']],
121
+ __someOtherMetadata: 'value'
122
+ };
123
+
124
+ const sqliteQuery = new MigrationSQLiteQuery();
125
+ const sql = sqliteQuery.createTable(tableWithMetadata);
126
+
127
+ assert(!sql.includes('undefined undefined'),
128
+ `SQL should not contain "undefined undefined" but got: ${sql}`);
129
+
130
+ const columnCount = (sql.match(/,/g) || []).length + 1;
131
+ assert(columnCount === 3, `Should have exactly 3 columns (id, organization_id, name), got ${columnCount}`);
132
+ });
133
+
134
+ console.log("Testing relationships filtering...\n");
135
+
136
+ test('Should handle table with relationships correctly', () => {
137
+ const tableWithRelations = {
138
+ __name: 'User',
139
+ id: {
140
+ name: 'id',
141
+ type: 'integer',
142
+ nullable: false,
143
+ primary: true
144
+ },
145
+ name: {
146
+ name: 'name',
147
+ type: 'string',
148
+ nullable: false
149
+ },
150
+ posts: {
151
+ name: 'posts',
152
+ type: 'hasMany',
153
+ target: 'Post'
154
+ },
155
+ profile: {
156
+ name: 'profile',
157
+ type: 'hasOne',
158
+ target: 'Profile'
159
+ },
160
+ indexes: ['idx_user_name']
161
+ };
162
+
163
+ const sqliteQuery = new MigrationSQLiteQuery();
164
+ const sql = sqliteQuery.createTable(tableWithRelations);
165
+
166
+ assert(!sql.includes('hasMany'), 'Should not include hasMany');
167
+ assert(!sql.includes('hasOne'), 'Should not include hasOne');
168
+ assert(!sql.includes('undefined undefined'),
169
+ `SQL should not contain "undefined undefined" but got: ${sql}`);
170
+ assert(sql.includes('id'), 'Should include id column');
171
+ assert(sql.includes('name'), 'Should include name column');
172
+ assert(!sql.includes('posts'), 'Should not include posts relationship');
173
+ assert(!sql.includes('profile'), 'Should not include profile relationship');
174
+
175
+ console.log(` Generated SQL: ${sql}\n`);
176
+ });
177
+
178
+ console.log("\n" + "=".repeat(64));
179
+ console.log(`Test Results: ${passed} passed, ${failed} failed`);
180
+ console.log("=".repeat(64));
181
+
182
+ if (failed > 0) {
183
+ console.log("\nāŒ Some tests failed!");
184
+ process.exit(1);
185
+ } else {
186
+ console.log("\nāœ… All tests passed!");
187
+ process.exit(0);
188
+ }
@@ -0,0 +1,306 @@
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 Results Summary
296
+ // =============================================================================
297
+ console.log("\n" + "=".repeat(68));
298
+ console.log(`\nšŸ“Š Test Results: ${passed} passed, ${failed} failed\n`);
299
+
300
+ if (failed === 0) {
301
+ console.log("āœ… All tests passed! v0.3.34 fixes are working correctly.\n");
302
+ process.exit(0);
303
+ } else {
304
+ console.log(`āš ļø ${failed} test(s) failed. Please review the output above.\n`);
305
+ process.exit(1);
306
+ }
@@ -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);