masterrecord 0.3.31 → 0.3.33
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/Migrations/cli.js +17 -4
- package/Migrations/dependencyGraph.js +108 -0
- package/Migrations/migrationMySQLQuery.js +5 -0
- package/Migrations/migrationPostgresQuery.js +5 -0
- package/Migrations/migrationSQLiteQuery.js +6 -1
- package/Migrations/migrationTemplate.js +144 -0
- package/Migrations/migrations.js +42 -10
- package/context.js +205 -0
- package/package.json +1 -1
- package/readme.md +574 -0
- package/test/index-bug-fix-test.js +188 -0
- package/test/seed-data-test.js +212 -0
- package/test/seed-features-integration-test.js +418 -0
- package/test/seed-migration-template-test.js +220 -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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: Context-Level Seed Data API
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the seed data API works correctly:
|
|
5
|
+
* - Chainable .seed() method
|
|
6
|
+
* - Array and single object support
|
|
7
|
+
* - Backward compatibility
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
console.log("╔════════════════════════════════════════════════════════════════╗");
|
|
11
|
+
console.log("║ Context-Level Seed Data API Test ║");
|
|
12
|
+
console.log("╚════════════════════════════════════════════════════════════════╝\n");
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
let failed = 0;
|
|
16
|
+
|
|
17
|
+
// Simulate the context class with seed data functionality
|
|
18
|
+
class SimulatedContext {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.__contextSeedData = {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
dbset(model, name) {
|
|
24
|
+
const tableName = name || model.name;
|
|
25
|
+
|
|
26
|
+
// Simulate entity registration
|
|
27
|
+
// (simplified - real implementation does more)
|
|
28
|
+
|
|
29
|
+
// Return chainable object with seed() method
|
|
30
|
+
return {
|
|
31
|
+
seed: (data) => this.#addSeedData(tableName, data)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#addSeedData(tableName, data) {
|
|
36
|
+
// Initialize seed data storage if not exists
|
|
37
|
+
if (!this.__contextSeedData) {
|
|
38
|
+
this.__contextSeedData = {};
|
|
39
|
+
}
|
|
40
|
+
if (!this.__contextSeedData[tableName]) {
|
|
41
|
+
this.__contextSeedData[tableName] = [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Handle both single object and array of objects
|
|
45
|
+
const records = Array.isArray(data) ? data : [data];
|
|
46
|
+
this.__contextSeedData[tableName].push(...records);
|
|
47
|
+
|
|
48
|
+
// Return chainable object for more .seed() calls
|
|
49
|
+
return {
|
|
50
|
+
seed: (moreData) => this.#addSeedData(tableName, moreData)
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Test entities
|
|
56
|
+
class User {
|
|
57
|
+
static name = 'User';
|
|
58
|
+
}
|
|
59
|
+
class Post {
|
|
60
|
+
static name = 'Post';
|
|
61
|
+
}
|
|
62
|
+
class Category {
|
|
63
|
+
static name = 'Category';
|
|
64
|
+
}
|
|
65
|
+
class Tag {
|
|
66
|
+
static name = 'Tag';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function test(description, fn) {
|
|
70
|
+
try {
|
|
71
|
+
fn();
|
|
72
|
+
console.log(`✓ ${description}`);
|
|
73
|
+
passed++;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.log(`✗ ${description}`);
|
|
76
|
+
console.log(` Error: ${error.message}`);
|
|
77
|
+
failed++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function assertEquals(actual, expected, message) {
|
|
82
|
+
if (actual !== expected) {
|
|
83
|
+
throw new Error(`${message}: expected ${expected}, got ${actual}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertDefined(value, message) {
|
|
88
|
+
if (value === undefined || value === null) {
|
|
89
|
+
throw new Error(`${message}: value is ${value}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run tests
|
|
94
|
+
console.log("Running tests...\n");
|
|
95
|
+
|
|
96
|
+
test("Context initializes with empty seed data", () => {
|
|
97
|
+
const ctx = new SimulatedContext();
|
|
98
|
+
assertDefined(ctx.__contextSeedData, "Context should have __contextSeedData");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("Single seed record is stored correctly", () => {
|
|
102
|
+
const ctx = new SimulatedContext();
|
|
103
|
+
ctx.dbset(User).seed({
|
|
104
|
+
name: 'Admin User',
|
|
105
|
+
email: 'admin@example.com'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
assertDefined(ctx.__contextSeedData.User, "User seed data should exist");
|
|
109
|
+
assertEquals(ctx.__contextSeedData.User.length, 1, "Should have 1 User record");
|
|
110
|
+
assertEquals(ctx.__contextSeedData.User[0].name, 'Admin User', "User name should match");
|
|
111
|
+
assertEquals(ctx.__contextSeedData.User[0].email, 'admin@example.com', "User email should match");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("Chainable seed calls accumulate data", () => {
|
|
115
|
+
const ctx = new SimulatedContext();
|
|
116
|
+
ctx.dbset(Post)
|
|
117
|
+
.seed({ title: 'First Post', content: 'Hello World' })
|
|
118
|
+
.seed({ title: 'Second Post', content: 'Testing seed' });
|
|
119
|
+
|
|
120
|
+
assertDefined(ctx.__contextSeedData.Post, "Post seed data should exist");
|
|
121
|
+
assertEquals(ctx.__contextSeedData.Post.length, 2, "Should have 2 Post records");
|
|
122
|
+
assertEquals(ctx.__contextSeedData.Post[0].title, 'First Post', "First post title should match");
|
|
123
|
+
assertEquals(ctx.__contextSeedData.Post[1].title, 'Second Post', "Second post title should match");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("Array seed syntax works", () => {
|
|
127
|
+
const ctx = new SimulatedContext();
|
|
128
|
+
ctx.dbset(Category).seed([
|
|
129
|
+
{ name: 'Technology' },
|
|
130
|
+
{ name: 'Business' },
|
|
131
|
+
{ name: 'Science' }
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
assertDefined(ctx.__contextSeedData.Category, "Category seed data should exist");
|
|
135
|
+
assertEquals(ctx.__contextSeedData.Category.length, 3, "Should have 3 Category records");
|
|
136
|
+
assertEquals(ctx.__contextSeedData.Category[0].name, 'Technology', "First category should match");
|
|
137
|
+
assertEquals(ctx.__contextSeedData.Category[1].name, 'Business', "Second category should match");
|
|
138
|
+
assertEquals(ctx.__contextSeedData.Category[2].name, 'Science', "Third category should match");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("Backward compatibility - dbset without seed works", () => {
|
|
142
|
+
const ctx = new SimulatedContext();
|
|
143
|
+
const result = ctx.dbset(Tag); // No seed call
|
|
144
|
+
|
|
145
|
+
// Should return chainable object but Tag seed data shouldn't exist yet
|
|
146
|
+
assertDefined(result, "dbset should return an object");
|
|
147
|
+
assertDefined(result.seed, "returned object should have seed method");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("Multiple dbset calls for different entities", () => {
|
|
151
|
+
const ctx = new SimulatedContext();
|
|
152
|
+
|
|
153
|
+
ctx.dbset(User).seed({ name: 'Alice' });
|
|
154
|
+
ctx.dbset(Post).seed({ title: 'Post 1' });
|
|
155
|
+
ctx.dbset(Category).seed({ name: 'Tech' });
|
|
156
|
+
|
|
157
|
+
assertDefined(ctx.__contextSeedData.User, "User seed data should exist");
|
|
158
|
+
assertDefined(ctx.__contextSeedData.Post, "Post seed data should exist");
|
|
159
|
+
assertDefined(ctx.__contextSeedData.Category, "Category seed data should exist");
|
|
160
|
+
assertEquals(ctx.__contextSeedData.User.length, 1, "Should have 1 User");
|
|
161
|
+
assertEquals(ctx.__contextSeedData.Post.length, 1, "Should have 1 Post");
|
|
162
|
+
assertEquals(ctx.__contextSeedData.Category.length, 1, "Should have 1 Category");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("Mixed single and array seed calls", () => {
|
|
166
|
+
const ctx = new SimulatedContext();
|
|
167
|
+
|
|
168
|
+
ctx.dbset(Post)
|
|
169
|
+
.seed({ title: 'Post 1' })
|
|
170
|
+
.seed([
|
|
171
|
+
{ title: 'Post 2' },
|
|
172
|
+
{ title: 'Post 3' }
|
|
173
|
+
])
|
|
174
|
+
.seed({ title: 'Post 4' });
|
|
175
|
+
|
|
176
|
+
assertEquals(ctx.__contextSeedData.Post.length, 4, "Should have 4 Post records");
|
|
177
|
+
assertEquals(ctx.__contextSeedData.Post[0].title, 'Post 1', "Post 1 title should match");
|
|
178
|
+
assertEquals(ctx.__contextSeedData.Post[1].title, 'Post 2', "Post 2 title should match");
|
|
179
|
+
assertEquals(ctx.__contextSeedData.Post[2].title, 'Post 3', "Post 3 title should match");
|
|
180
|
+
assertEquals(ctx.__contextSeedData.Post[3].title, 'Post 4', "Post 4 title should match");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("Seed data is isolated per table", () => {
|
|
184
|
+
const ctx = new SimulatedContext();
|
|
185
|
+
|
|
186
|
+
ctx.dbset(User).seed({ name: 'User 1' });
|
|
187
|
+
ctx.dbset(User).seed({ name: 'User 2' });
|
|
188
|
+
ctx.dbset(Post).seed({ title: 'Post 1' });
|
|
189
|
+
|
|
190
|
+
assertEquals(ctx.__contextSeedData.User.length, 2, "Should have 2 User records");
|
|
191
|
+
assertEquals(ctx.__contextSeedData.Post.length, 1, "Should have 1 Post record");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("Empty array seed is handled", () => {
|
|
195
|
+
const ctx = new SimulatedContext();
|
|
196
|
+
ctx.dbset(Category).seed([]);
|
|
197
|
+
|
|
198
|
+
// Empty array should still create the table entry but with 0 records
|
|
199
|
+
assertEquals(ctx.__contextSeedData.Category.length, 0, "Should have 0 Category records");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Print summary
|
|
203
|
+
console.log("\n" + "─".repeat(64));
|
|
204
|
+
console.log(`Tests completed: ${passed} passed, ${failed} failed`);
|
|
205
|
+
|
|
206
|
+
if (failed > 0) {
|
|
207
|
+
console.log("\n❌ Some tests failed");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
} else {
|
|
210
|
+
console.log("\n✅ All tests passed!");
|
|
211
|
+
process.exit(0);
|
|
212
|
+
}
|