masterrecord 0.3.30 → 0.3.32
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/Entity/entityModelBuilder.js +44 -1
- package/Migrations/cli.js +17 -4
- package/Migrations/dependencyGraph.js +108 -0
- package/Migrations/migrationMySQLQuery.js +10 -0
- package/Migrations/migrationPostgresQuery.js +10 -0
- package/Migrations/migrationSQLiteQuery.js +10 -0
- package/Migrations/migrationTemplate.js +176 -0
- package/Migrations/migrations.js +121 -12
- package/Migrations/schema.js +63 -0
- package/context.js +288 -0
- package/package.json +1 -1
- package/readme.md +812 -37
- 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,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
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for all 5 seed data features
|
|
3
|
+
* Tests: Down Migrations, Conditional Seeding, Dependency Ordering, Factory Integration, Upsert Semantics
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const context = require('../context');
|
|
8
|
+
const Migration = require('../Migrations/migrations');
|
|
9
|
+
const MigrationTemplate = require('../Migrations/migrationTemplate');
|
|
10
|
+
const DependencyGraph = require('../Migrations/dependencyGraph');
|
|
11
|
+
|
|
12
|
+
// Mock entity definitions for testing
|
|
13
|
+
class User {
|
|
14
|
+
static __name = 'User';
|
|
15
|
+
static id = { type: 'integer', primary: true };
|
|
16
|
+
static name = { type: 'string' };
|
|
17
|
+
static email = { type: 'string' };
|
|
18
|
+
static org_id = {
|
|
19
|
+
type: 'integer',
|
|
20
|
+
relationshipType: 'belongsTo',
|
|
21
|
+
foreignTable: 'Organization',
|
|
22
|
+
foreignKey: 'org_id'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class Organization {
|
|
27
|
+
static __name = 'Organization';
|
|
28
|
+
static id = { type: 'integer', primary: true };
|
|
29
|
+
static name = { type: 'string' };
|
|
30
|
+
static slug = { type: 'string' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class Post {
|
|
34
|
+
static __name = 'Post';
|
|
35
|
+
static id = { type: 'integer', primary: true };
|
|
36
|
+
static title = { type: 'string' };
|
|
37
|
+
static user_id = {
|
|
38
|
+
type: 'integer',
|
|
39
|
+
relationshipType: 'belongsTo',
|
|
40
|
+
foreignTable: 'User',
|
|
41
|
+
foreignKey: 'user_id'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Test Context
|
|
46
|
+
class TestContext extends context {
|
|
47
|
+
constructor() {
|
|
48
|
+
super();
|
|
49
|
+
this.__entities = [User, Organization, Post];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('Seed Data Features - Integration Tests', function() {
|
|
54
|
+
|
|
55
|
+
describe('Feature 1: Down Migrations', function() {
|
|
56
|
+
it('should generate down migration code when enabled', function() {
|
|
57
|
+
const ctx = new TestContext();
|
|
58
|
+
ctx.seedConfig({
|
|
59
|
+
generateDownMigrations: true,
|
|
60
|
+
downStrategy: 'delete',
|
|
61
|
+
onRollbackError: 'warn'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
ctx.dbset(User).seed({ id: 1, name: 'Test User', email: 'test@example.com' });
|
|
65
|
+
|
|
66
|
+
const seedData = ctx.__contextSeedData;
|
|
67
|
+
assert.ok(seedData.User, 'User seed data should exist');
|
|
68
|
+
assert.strictEqual(seedData.User.length, 1);
|
|
69
|
+
assert.ok(seedData.User[0].__rollback, 'Rollback metadata should be attached');
|
|
70
|
+
assert.strictEqual(seedData.User[0].__rollback.key, 'id');
|
|
71
|
+
assert.strictEqual(seedData.User[0].__rollback.value, 1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should generate correct down migration template', function() {
|
|
75
|
+
const MT = new MigrationTemplate('TestMigration');
|
|
76
|
+
const records = [
|
|
77
|
+
{ id: 1, name: 'User 1', __rollback: { key: 'id', value: 1 } },
|
|
78
|
+
{ id: 2, name: 'User 2', __rollback: { key: 'id', value: 2 } }
|
|
79
|
+
];
|
|
80
|
+
const config = { generateDownMigrations: true, onRollbackError: 'warn' };
|
|
81
|
+
|
|
82
|
+
MT.seedDataDown('down', 'User', records, config);
|
|
83
|
+
const result = MT.get();
|
|
84
|
+
|
|
85
|
+
assert.ok(result.includes('async down(table)'), 'Should have down method');
|
|
86
|
+
assert.ok(result.includes('table.User.findById'), 'Should query by ID');
|
|
87
|
+
assert.ok(result.includes('await record.delete()'), 'Should delete record');
|
|
88
|
+
assert.ok(result.includes('console.warn'), 'Should warn on error');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should not generate down migration when disabled', function() {
|
|
92
|
+
const ctx = new TestContext();
|
|
93
|
+
ctx.seedConfig({ generateDownMigrations: false });
|
|
94
|
+
|
|
95
|
+
ctx.dbset(User).seed({ id: 1, name: 'Test' });
|
|
96
|
+
|
|
97
|
+
assert.ok(!ctx.__contextSeedData.User[0].__rollback, 'Should not have rollback metadata');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('Feature 2: Conditional Seeding', function() {
|
|
102
|
+
it('should attach environment metadata with .when()', function() {
|
|
103
|
+
const ctx = new TestContext();
|
|
104
|
+
ctx.dbset(User)
|
|
105
|
+
.seed({ name: 'Dev User' })
|
|
106
|
+
.when('development', 'test');
|
|
107
|
+
|
|
108
|
+
const seedData = ctx.__contextSeedData;
|
|
109
|
+
assert.ok(seedData.User[0].__seedEnv, 'Environment metadata should exist');
|
|
110
|
+
assert.deepStrictEqual(seedData.User[0].__seedEnv.conditions, ['development', 'test']);
|
|
111
|
+
assert.strictEqual(seedData.User[0].__seedEnv.strategy, 'generation-time');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should filter records by environment at generation time', function() {
|
|
115
|
+
const MT = new MigrationTemplate('TestMigration');
|
|
116
|
+
const records = [
|
|
117
|
+
{ name: 'Prod User', __seedEnv: { conditions: ['production'], strategy: 'generation-time' } },
|
|
118
|
+
{ name: 'Dev User', __seedEnv: { conditions: ['development'], strategy: 'generation-time' } },
|
|
119
|
+
{ name: 'All User' }
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
MT.seedData('up', 'User', records, 'development');
|
|
123
|
+
const result = MT.get();
|
|
124
|
+
|
|
125
|
+
assert.ok(!result.includes('Prod User'), 'Should not include production-only record');
|
|
126
|
+
assert.ok(result.includes('Dev User'), 'Should include development record');
|
|
127
|
+
assert.ok(result.includes('All User'), 'Should include unconditional record');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should support chaining with seed()', function() {
|
|
131
|
+
const ctx = new TestContext();
|
|
132
|
+
ctx.dbset(User)
|
|
133
|
+
.seed({ name: 'User 1' })
|
|
134
|
+
.when('development')
|
|
135
|
+
.seed({ name: 'User 2' });
|
|
136
|
+
|
|
137
|
+
assert.strictEqual(ctx.__contextSeedData.User.length, 2);
|
|
138
|
+
assert.ok(ctx.__contextSeedData.User[0].__seedEnv, 'First record should have env condition');
|
|
139
|
+
assert.ok(!ctx.__contextSeedData.User[1].__seedEnv, 'Second record should not have env condition');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('Feature 3: Dependency Ordering', function() {
|
|
144
|
+
it('should build dependency graph from entities', function() {
|
|
145
|
+
const graph = new DependencyGraph([User, Organization, Post]);
|
|
146
|
+
graph.buildFromEntities();
|
|
147
|
+
|
|
148
|
+
assert.ok(graph.graph.has('User'), 'Should have User in graph');
|
|
149
|
+
assert.ok(graph.graph.has('Organization'), 'Should have Organization in graph');
|
|
150
|
+
assert.ok(graph.graph.has('Post'), 'Should have Post in graph');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should perform topological sort correctly', function() {
|
|
154
|
+
const graph = new DependencyGraph([User, Organization, Post]);
|
|
155
|
+
graph.buildFromEntities();
|
|
156
|
+
const sorted = graph.topologicalSort();
|
|
157
|
+
|
|
158
|
+
const orgIndex = sorted.indexOf('Organization');
|
|
159
|
+
const userIndex = sorted.indexOf('User');
|
|
160
|
+
const postIndex = sorted.indexOf('Post');
|
|
161
|
+
|
|
162
|
+
assert.ok(orgIndex < userIndex, 'Organization should come before User');
|
|
163
|
+
assert.ok(userIndex < postIndex, 'User should come before Post');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should filter to seeded tables only', function() {
|
|
167
|
+
const graph = new DependencyGraph([User, Organization, Post]);
|
|
168
|
+
graph.buildFromEntities();
|
|
169
|
+
const seedData = { User: [], Post: [] }; // Only User and Post have seeds
|
|
170
|
+
const filtered = graph.filterToSeededTables(seedData);
|
|
171
|
+
|
|
172
|
+
assert.strictEqual(filtered.length, 2, 'Should only include seeded tables');
|
|
173
|
+
assert.ok(filtered.includes('User'), 'Should include User');
|
|
174
|
+
assert.ok(filtered.includes('Post'), 'Should include Post');
|
|
175
|
+
assert.ok(!filtered.includes('Organization'), 'Should not include Organization');
|
|
176
|
+
assert.ok(filtered.indexOf('User') < filtered.indexOf('Post'), 'User should come before Post');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle circular dependencies gracefully', function() {
|
|
180
|
+
// Create entities with circular dependency
|
|
181
|
+
class A {
|
|
182
|
+
static __name = 'A';
|
|
183
|
+
static b_id = { relationshipType: 'belongsTo', foreignTable: 'B' };
|
|
184
|
+
}
|
|
185
|
+
class B {
|
|
186
|
+
static __name = 'B';
|
|
187
|
+
static a_id = { relationshipType: 'belongsTo', foreignTable: 'A' };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const graph = new DependencyGraph([A, B]);
|
|
191
|
+
graph.buildFromEntities();
|
|
192
|
+
|
|
193
|
+
assert.throws(() => {
|
|
194
|
+
graph.topologicalSort();
|
|
195
|
+
}, /Circular dependency/, 'Should throw error on circular dependency');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should order seed data via getOrderedSeedData()', function() {
|
|
199
|
+
const ctx = new TestContext();
|
|
200
|
+
ctx.dbset(Post).seed({ title: 'Post 1', user_id: 1 });
|
|
201
|
+
ctx.dbset(User).seed({ id: 1, name: 'User 1', org_id: 1 });
|
|
202
|
+
ctx.dbset(Organization).seed({ id: 1, name: 'Org 1' });
|
|
203
|
+
|
|
204
|
+
const ordered = ctx.getOrderedSeedData();
|
|
205
|
+
const keys = Object.keys(ordered);
|
|
206
|
+
|
|
207
|
+
assert.strictEqual(keys.length, 3);
|
|
208
|
+
assert.strictEqual(keys[0], 'Organization', 'Organization should be first');
|
|
209
|
+
assert.strictEqual(keys[1], 'User', 'User should be second');
|
|
210
|
+
assert.strictEqual(keys[2], 'Post', 'Post should be third');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe('Feature 4: Factory Integration', function() {
|
|
215
|
+
it('should generate records with seedFactory()', function() {
|
|
216
|
+
const ctx = new TestContext();
|
|
217
|
+
ctx.dbset(User).seedFactory(5, i => ({
|
|
218
|
+
id: i + 1,
|
|
219
|
+
name: `User ${i}`,
|
|
220
|
+
email: `user${i}@example.com`
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
const seedData = ctx.__contextSeedData;
|
|
224
|
+
assert.strictEqual(seedData.User.length, 5, 'Should generate 5 records');
|
|
225
|
+
assert.strictEqual(seedData.User[0].name, 'User 0');
|
|
226
|
+
assert.strictEqual(seedData.User[4].name, 'User 4');
|
|
227
|
+
assert.ok(seedData.User[0].__seedMeta?.generated, 'Should mark as factory-generated');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should throw error if generator is not a function', function() {
|
|
231
|
+
const ctx = new TestContext();
|
|
232
|
+
assert.throws(() => {
|
|
233
|
+
ctx.dbset(User).seedFactory(5, 'not a function');
|
|
234
|
+
}, /requires a generator function/);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should throw error if count is invalid', function() {
|
|
238
|
+
const ctx = new TestContext();
|
|
239
|
+
assert.throws(() => {
|
|
240
|
+
ctx.dbset(User).seedFactory(-1, i => ({ name: `User ${i}` }));
|
|
241
|
+
}, /requires a positive number/);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should generate loop syntax for 10+ factory records', function() {
|
|
245
|
+
const MT = new MigrationTemplate('TestMigration');
|
|
246
|
+
const records = Array.from({ length: 10 }, (_, i) => ({
|
|
247
|
+
name: `User ${i}`,
|
|
248
|
+
__seedMeta: { generated: true, index: i }
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
MT.seedData('up', 'User', records, 'development');
|
|
252
|
+
const result = MT.get();
|
|
253
|
+
|
|
254
|
+
assert.ok(result.includes('const factoryRecords = ['), 'Should use array syntax');
|
|
255
|
+
assert.ok(result.includes('for (const record of factoryRecords)'), 'Should use loop');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should use individual inserts for <10 factory records', function() {
|
|
259
|
+
const MT = new MigrationTemplate('TestMigration');
|
|
260
|
+
const records = Array.from({ length: 3 }, (_, i) => ({
|
|
261
|
+
name: `User ${i}`,
|
|
262
|
+
__seedMeta: { generated: true, index: i }
|
|
263
|
+
}));
|
|
264
|
+
|
|
265
|
+
MT.seedData('up', 'User', records, 'development');
|
|
266
|
+
const result = MT.get();
|
|
267
|
+
|
|
268
|
+
assert.ok(!result.includes('const factoryRecords'), 'Should not use array syntax');
|
|
269
|
+
assert.ok(result.includes('await table.User.create'), 'Should use individual creates');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('Feature 5: Upsert Semantics', function() {
|
|
274
|
+
it('should attach upsert metadata with .upsert()', function() {
|
|
275
|
+
const ctx = new TestContext();
|
|
276
|
+
ctx.dbset(User)
|
|
277
|
+
.seed({ id: 1, name: 'Admin' })
|
|
278
|
+
.upsert();
|
|
279
|
+
|
|
280
|
+
const seedData = ctx.__contextSeedData;
|
|
281
|
+
assert.ok(seedData.User[0].__seedStrategy, 'Should have upsert strategy');
|
|
282
|
+
assert.strictEqual(seedData.User[0].__seedStrategy.type, 'upsert');
|
|
283
|
+
assert.strictEqual(seedData.User[0].__seedStrategy.conflictKey, 'primaryKey');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should support custom conflict key', function() {
|
|
287
|
+
const ctx = new TestContext();
|
|
288
|
+
ctx.dbset(User)
|
|
289
|
+
.seed({ email: 'admin@example.com', name: 'Admin' })
|
|
290
|
+
.upsert({ conflictKey: 'email' });
|
|
291
|
+
|
|
292
|
+
const strategy = ctx.__contextSeedData.User[0].__seedStrategy;
|
|
293
|
+
assert.strictEqual(strategy.conflictKey, 'email');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should support partial updates', function() {
|
|
297
|
+
const ctx = new TestContext();
|
|
298
|
+
ctx.dbset(User)
|
|
299
|
+
.seed({ id: 1, name: 'Updated Name', email: 'old@example.com' })
|
|
300
|
+
.upsert({ updateFields: ['name'] });
|
|
301
|
+
|
|
302
|
+
const strategy = ctx.__contextSeedData.User[0].__seedStrategy;
|
|
303
|
+
assert.deepStrictEqual(strategy.updateFields, ['name']);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should generate check-then-update code', function() {
|
|
307
|
+
const MT = new MigrationTemplate('TestMigration');
|
|
308
|
+
const records = [{
|
|
309
|
+
id: 1,
|
|
310
|
+
name: 'Admin',
|
|
311
|
+
email: 'admin@example.com',
|
|
312
|
+
__seedStrategy: { type: 'upsert', conflictKey: 'primaryKey', updateFields: null }
|
|
313
|
+
}];
|
|
314
|
+
|
|
315
|
+
MT.seedData('up', 'User', records, 'development');
|
|
316
|
+
const result = MT.get();
|
|
317
|
+
|
|
318
|
+
assert.ok(result.includes('const existing = await table.User.where'), 'Should check for existing');
|
|
319
|
+
assert.ok(result.includes('if (existing)'), 'Should have conditional');
|
|
320
|
+
assert.ok(result.includes('await existing.save()'), 'Should save on update');
|
|
321
|
+
assert.ok(result.includes('await table.User.create'), 'Should create if not exists');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should apply default upsert strategy when configured', function() {
|
|
325
|
+
const ctx = new TestContext();
|
|
326
|
+
ctx.seedConfig({ defaultStrategy: 'upsert' });
|
|
327
|
+
ctx.dbset(User).seed({ id: 1, name: 'Admin' });
|
|
328
|
+
|
|
329
|
+
const strategy = ctx.__contextSeedData.User[0].__seedStrategy;
|
|
330
|
+
assert.strictEqual(strategy.type, 'upsert');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Combined Features', function() {
|
|
335
|
+
it('should support all features together', function() {
|
|
336
|
+
const ctx = new TestContext();
|
|
337
|
+
ctx.seedConfig({
|
|
338
|
+
generateDownMigrations: true,
|
|
339
|
+
defaultStrategy: 'upsert',
|
|
340
|
+
detectCircularDependencies: true,
|
|
341
|
+
circularStrategy: 'warn'
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Feature 3 (Dependency Ordering) + Feature 4 (Factory) + Feature 2 (Conditional) + Feature 5 (Upsert)
|
|
345
|
+
ctx.dbset(Organization)
|
|
346
|
+
.seed({ id: 1, name: 'Default Org', slug: 'default' })
|
|
347
|
+
.upsert({ conflictKey: 'slug' });
|
|
348
|
+
|
|
349
|
+
ctx.dbset(User)
|
|
350
|
+
.seedFactory(3, i => ({ id: i + 1, name: `Admin ${i}`, org_id: 1 }))
|
|
351
|
+
.when('development')
|
|
352
|
+
.upsert();
|
|
353
|
+
|
|
354
|
+
ctx.dbset(Post)
|
|
355
|
+
.seed({ id: 1, title: 'Welcome', user_id: 1 })
|
|
356
|
+
.when('development');
|
|
357
|
+
|
|
358
|
+
const ordered = ctx.getOrderedSeedData();
|
|
359
|
+
const keys = Object.keys(ordered);
|
|
360
|
+
|
|
361
|
+
// Verify ordering
|
|
362
|
+
assert.strictEqual(keys[0], 'Organization');
|
|
363
|
+
assert.strictEqual(keys[1], 'User');
|
|
364
|
+
assert.strictEqual(keys[2], 'Post');
|
|
365
|
+
|
|
366
|
+
// Verify features applied
|
|
367
|
+
assert.ok(ordered.Organization[0].__seedStrategy, 'Org should have upsert');
|
|
368
|
+
assert.ok(ordered.User[0].__seedMeta?.generated, 'User should be factory-generated');
|
|
369
|
+
assert.ok(ordered.User[0].__seedEnv, 'User should have env condition');
|
|
370
|
+
assert.ok(ordered.Post[0].__seedEnv, 'Post should have env condition');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should chain all methods together', function() {
|
|
374
|
+
const ctx = new TestContext();
|
|
375
|
+
ctx.dbset(User)
|
|
376
|
+
.seedFactory(5, i => ({ id: i + 1, name: `User ${i}` }))
|
|
377
|
+
.when('development', 'test')
|
|
378
|
+
.upsert({ conflictKey: 'id' });
|
|
379
|
+
|
|
380
|
+
const record = ctx.__contextSeedData.User[0];
|
|
381
|
+
assert.ok(record.__seedMeta?.generated, 'Should have factory metadata');
|
|
382
|
+
assert.ok(record.__seedEnv, 'Should have environment metadata');
|
|
383
|
+
assert.ok(record.__seedStrategy, 'Should have upsert strategy');
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
describe('Backward Compatibility', function() {
|
|
388
|
+
it('should work without any configuration', function() {
|
|
389
|
+
const ctx = new TestContext();
|
|
390
|
+
ctx.dbset(User).seed({ name: 'Simple User' });
|
|
391
|
+
|
|
392
|
+
assert.ok(ctx.__contextSeedData.User);
|
|
393
|
+
assert.strictEqual(ctx.__contextSeedData.User.length, 1);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should support array syntax', function() {
|
|
397
|
+
const ctx = new TestContext();
|
|
398
|
+
ctx.dbset(User).seed([
|
|
399
|
+
{ name: 'User 1' },
|
|
400
|
+
{ name: 'User 2' }
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
assert.strictEqual(ctx.__contextSeedData.User.length, 2);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should support chaining without features', function() {
|
|
407
|
+
const ctx = new TestContext();
|
|
408
|
+
ctx.dbset(User)
|
|
409
|
+
.seed({ name: 'User 1' })
|
|
410
|
+
.seed({ name: 'User 2' });
|
|
411
|
+
|
|
412
|
+
assert.strictEqual(ctx.__contextSeedData.User.length, 2);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
console.log('✓ All seed data integration tests defined');
|
|
418
|
+
console.log('Run with: npm test or node test/seed-features-integration-test.js');
|